Logbook Example for Software Tasks
Logbook Example for Software Tasks
Simon Roadknight 1
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Table of contents
TABLE OF CONTENTS 2
T1 – GHERKIN SPECIFICATIONS 4
Errors identified 5
Some Assumptions about what a valid file path and song file are 5
Why the program will not play any songs if errors are found when there are multiple song files 10
T1 –DATA MODELLING 19
Simon Roadknight 2
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Behaviour step: Playing a single song, and multiple songs holistic view 35
T1 - REFLECTION 50
T2 - IMPLEMENTATION 51
T2 - REFLECTION 71
T3 – TESTING 72
Manual testing 72
Automated testing 85
T1 - REFLECTION 88
REFERENCE LIST 92
Simon Roadknight 3
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
T1 – Gherkin Specifications
What are Gherkin Specifications?
Gherkin specification is often used in Behaviour-Driven Development (BDD) which puts focus on
feature behaviours. Gherkin allows us to focus on our program’s behaviour features, these are
features of our program that capture how the user will interacts and uses it (Knight, 2017). We can
take those features and write well-defined scenarios using inputs, actions, and outcomes. This is
built upon Hoare logic, or more specifically the Hoare triple where a precondition and postcondition
are asserted, and a command is specified, when the precondition is met, and the command is
executed, the postcondition is established. Writing Gherkin specifications allow us to understand
our specification better, capture the behaviour features of our program, and better plan what data
and behaviours our program will have when we look at our data modelling and behaviour steps. We
are also able to use our Gherkin specification for testing, specifically user focused testing.
From the song player specification given, I have identified two behaviour features, to play a single
song, and multiple songs via input arguments to the program. The user is a system administrator and
want to be able to use the program either manually or autonomously using scripts, this is the reason
the program will not be interactive.
Simon Roadknight 4
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Errors identified
The following errors have been identified for the Gherkin specifications outlined below.
Table 2 Errors identified
Some Assumptions about what a valid file path and song file are
There are two assumptions that I will be making in the following features and gherkin scenarios.
1. Valid song file: A valid song file going forward is a file that contains the correct data to be
parsed and played as a song using our song player program.
2. Valid file path: A valid file path going forward is any file path that both is syntactically correct
file path for the OS specified AND where a file IS found, even if the contents of the file is not
a valid song file.
Simon Roadknight 5
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Feature narrative
We have identified the first feature, the kind of user, what they want, and why they want it.
Scenario: Execute the song player program using a valid file path argument to a single valid
song file containing a valid song
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt
Executing the Songplayer with a valid file path to a valid song file.
Scenario specification
Scenario Outline: Execute the song player program using a valid path argument to a single
valid song file
Figure 2 Executing the song player with a single valid song file
This is the only happy path for playing a single argument, where the file path is valid, and the file
contains a valid song. The play time for the song is calculated and then printed out to the standard
output. There are two scenarios, one with a Windows file path and one with a Linux file path (Each
Simon Roadknight 6
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
scenario and scenario out where there are file paths will also follow this pattern). It is stated in the
specification the System Administrator may be on Windows and/or Linux.
Example usage
Songplayer.exe
Executing the Songplayer with no input arguments.
Scenario specification
Scenario: Execute the song player program without specifying a file path
This is the scenario for when the song player is executed with no arguments.
Simon Roadknight 7
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using a valid file path argument to a single invalid
song file
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\invalidsong1.txt
Executing the Songplayer with a valid file path to an invalid song.
Scenario specification
Scenario Outline: Execute the song player program using a valid file path argument to a
single invalid song file
This is the scenario outline for when a valid file path is entered as an input argument, but the file is
an invalid song file.
Simon Roadknight 8
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using an invalid file path argument
Example usage
Songplayer.exe C://\\
The Songplayer program being run with an invalid file path.
Scenario specification
Scenario Outline: Execute the song player program using an invalid file path argument
This is the scenario outline for when an invalid file path is provided.
Simon Roadknight 9
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Why the program will not play any songs if errors are found when there are multiple
song files
I have made the decision that if a single input argument is an invalid file path, invalid song file or
could not be opened for IO and permission reasons, then no songs will play. I have made this
decision as this is a noninteractive program, and the user may be expecting songs to play in a very
specific order to be alerted correctly of the system event that is taking place. If we were to play
some songs and not others, this could misrepresent the event taking place, and render the custom
alerts unusable.
Feature narrative
We have identified the second feature, the kind of user, what they want, and why they want it.
Simon Roadknight 10
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using two valid file path arguments to two valid
song files
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt
C:\users\anexampleuser\songfiles\song2.txt
Executing the Songplayer program with two valid file paths containing two valid song files.
Scenario specification
Scenario Outline: Execute the song player program using two valid file path arguments to
two valid song files
This is the scenario outline for when all input arguments provided are valid file paths that contain
valid song files.
Simon Roadknight 11
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using two valid file path arguments to two invalid
song files
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\invalidsong1.txt
C:\users\anexampleuser\songfiles\invalidsong2.txt
Executing the Songplayer program with two invalid song files as input arguments.
Scenario specification
Scenario Outline: Execute the song player program using two valid file path arguments to
two invalid song files
This is the scenario outline for when two valid paths are specified that contain two invalid song files.
This shows how we can deal with multiple errors.
Simon Roadknight 12
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using one valid file path argument to one valid
song file and one valid file path argument to one invalid song file
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt C:\users\anexampleuser\songfiles\
invalidsong2.txt
Executing the Songplayer program with one input argument that represents a valid song file and one
that does not.
Scenario specification
Scenario Outline: Execute the song player program using one valid file path argument to
one valid song file and one valid file path argument to one invalid song file
Figure 9 Scenario for one valid song and one invalid song
This is the scenario outline for when there is one valid song file and one invalid song file. This shows
how we can deal with a mixed input of valid and invalid arguments.
Simon Roadknight 13
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using two invalid file path arguments
Example usage
Songplayer.exe C://\\ C://\\2
Executing the Songplayer with two invalid file paths.
Scenario specification
Scenario Outline: Execute the song player program using two invalid file path arguments
This is the scenario outline for when there are two invalid file paths.
Simon Roadknight 14
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using one valid file path argument to one valid
song file and one invalid file path argument
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt C:\\//
Executing the Songplayer program with one valid song file and one invalid file path.
Scenario specification
Scenario Outline: Execute the song player program using one valid path argument to one
valid song file and one invalid file path argument
Figure 11 Scenario for one valid song and one invalid file path
This is the scenario outline for when there is one valid song file and one invalid file path argument.
Simon Roadknight 15
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using three valid file path arguments to three
valid song files
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt
C:\users\anexampleuser\songfiles\song2.txt
C:\users\anexampleuser\songfiles\song3.txt
Executing the Songplayer program with three input arguments that are all valid file paths that
contain valid song files.
Scenario specification
Scenario Outline: Execute the song player program using three valid file path arguments to
three valid song files
This is the scenario outline for when all input arguments provided are valid file paths that contain
valid song files. This shows how we would deal with ‘N’ number of valid songs, where ‘N’ is the
number of valid input arguments.
Simon Roadknight 16
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using a mixture of valid and invalid file path
arguments to a mixture of valid and invalid song files
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\song1.txt
C:\users\anexampleuser\songfiles\invalidsong1.txt
C:\\//
Executing the Songplayer program with three input arguments where there is a mix of valid and
invalid arguments.
Scenario specification
Scenario Outline: Execute the song player program using a mixture of valid and invalid file
path arguments to a mixture of valid and invalid song files
This is the scenario outline for when there are a mix of valid and invalid argument inputs. This
demonstrates we can handle multiple invalid songs, even when they are mixed in with valid songs.
Simon Roadknight 17
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Scenario: Execute the song player program using three valid file paths, one to a file where
there are invalid file permissions, one to an empty file and one to a file which will cause an
I/O error
Example usage
Songplayer.exe C:\users\anexampleuser\songfiles\invalidsong3.txt
C:\users\anexampleuser\songfiles\invalidsong4.txt
C:\users\anexampleuser\songfiles\invalidsong5.txt
Executing the Songplayer program with three input arguments that are invalid.
Scenario specification
Scenario Outline: Execute the song player program using three invalid file paths, one to a
file where we have invalid file permissions, one to an empty file and one to a file which will
cause an I/O error
This scenario covers the errors identified not covered by other scenarios.
Simon Roadknight 18
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
T1 –Data Modelling
From the gherkin specification this is the data I have identified that we need to model, this will allow
us to read song files, parse songs, play songs, display to the output the total song play time and
finally, output any errors we found with the song files along the way.
Simon Roadknight 19
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
In F# we can use type aliases, that allow us to define a new name for a type that already exists
(Wlaschin, 2012). This allows for useful naming in our code, reducing cognitive overload and serving
as living documentation.
Simon Roadknight 20
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
We can create our type alias for string [] by using our previously defined Argument type, we will
have Arguments which is an Argument [].
Simon Roadknight 21
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
𝑊𝑖𝑛𝑑𝑜𝑤𝑠𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ: string
𝑊𝑖𝑛𝑑𝑜𝑤𝑠𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ = "^[𝑎 − 𝑧𝐴 − 𝑍]:\\[\\\𝑆| ∗\𝑆]? .∗ $"
𝐿𝑖𝑛𝑢𝑥𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ: string
𝐿𝑖𝑛𝑢𝑥𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ = "^(/[^/ ] ∗) +/? $"
FilePath can be defined as the set of all WindowsFilePath and LinuxFilePath defined above.
𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ: ℙ string
𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ = {𝑥: |
𝑠𝑡𝑟𝑖𝑛𝑔 𝑥 = 𝑊𝑖𝑛𝑑𝑜𝑤𝑠𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ ∨ 𝑥 = 𝐿𝑖𝑛𝑢𝑥𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ}
Implementing it in F#
type FilePath = private FilePath of string
Here we have defined a type called FilePath that provides a single discriminated union case
‘FilePath’, which is a subset of sequence of string. We have used the ‘private’ access modifier to
prevent FilePath values being constructed from sequences of strings that do not meet our
constraints. The only way to create a FilePath is through the constructor that is in the same module
the type is defines, this will stand true for the rest of our constrained types.
Simon Roadknight 22
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
: 𝑏𝑒𝑒𝑝 𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦 =< 𝑛𝑢𝑚1 > 𝑙𝑒𝑛𝑔𝑡ℎ =< 𝑛𝑢𝑚2 > 𝑚𝑠;
𝑏𝑒𝑒𝑝𝑃𝑎𝑡𝑡𝑒𝑟𝑛: string
𝑏𝑒𝑒𝑝𝑃𝑎𝑡𝑡𝑒𝑟𝑛 = "^\𝑠 ∗: 𝑏𝑒𝑒𝑝\𝑠 + 𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦 = (\𝑑+)\𝑠 + 𝑙𝑒𝑛𝑔𝑡ℎ = (\𝑑+)𝑚𝑠;\𝑠 ∗ $"
Implementing it in F#
We can define the beepPattern as the string:
"^\s*:beep\s+frequency=(\d+)\s+length=(\d+)ms;?\s*$"
Simon Roadknight 23
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
OR
𝑑𝑒𝑙𝑎𝑦𝑃𝑎𝑡𝑡𝑒𝑟𝑛: string
𝑑𝑒𝑙𝑎𝑦𝑃𝑎𝑡𝑡𝑒𝑟𝑛 = "^\𝑠 ∗: 𝑑𝑒𝑙𝑎𝑦\𝑠 + (\𝑑+)𝑚𝑠; ?\𝑠 ∗ $"
Implementing it in F#
We can define delayPattern as the string: "^\s*:delay\s+(\d+)ms;?\s*$"
Implementing it in F#
We can define whitespacePattern as the string: "^\s*$"
Simon Roadknight 24
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
type SongFile = private SongFile of seq<string>
Simon Roadknight 25
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 26
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑖𝑛 ∶ 𝑢𝑖𝑛𝑡
𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑖𝑛 = 31
𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑎𝑥 ∶ 𝑢𝑖𝑛𝑡
𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑎𝑥 = 19000
𝐹𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦: ℙ uint
𝐹𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦 = {𝑥: 𝑢𝑖𝑛𝑡 | 𝑥 ≥ 𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑖𝑛 ∧ 𝑥 ≤ 𝑓𝑟𝑒𝑞𝑢𝑒𝑛𝑐𝑦𝑀𝑎𝑥}
Implementing it in F#
We can define Frequency as a single discriminated union case ‘Frequency’, which is a subset of uint.
Simon Roadknight 27
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑖𝑛 ∶ 𝑢𝑖𝑛𝑡
𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑖𝑛 = 50
𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑎𝑥 ∶ 𝑢𝑖𝑛𝑡
𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑎𝑥 = 5000
𝑇𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ: ℙ uint
𝑇𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ = {𝑥: 𝑢𝑖𝑛𝑡 |𝑥 ≥ 𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑖𝑛 ∧ 𝑥 ≤ 𝑡𝑖𝑚𝑒𝐿𝑒𝑛𝑔𝑡ℎ𝑀𝑎𝑥 }
TimeLength is a subset of the set uint, with a range between 50ms and 5000ms. I have chosen 50ms
as the minimum time length as it gives sometime between a beep can change, and it is a valid
minimum range from the song examples. For the maximum, I didn’t want a beep to be able to play
too long, and there were no delays greater than 5000ms in the example song files.
Implementing it in F#
We can define TimeLength as a single discriminated union case ‘TimeLength’, which is a subset of
uint.
Simon Roadknight 28
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
We can define Beep as a single discriminated union case ‘Beep’, which is the set of all Frequency
paired with the set of all TimeLength.
Implementing it in F#
We can define Delay as a single discriminated union case ‘Delay’, which is the set of all TimeLength.
Simon Roadknight 29
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
We can use a discriminated union to create our SongInstruction type. This allows us to have values
for multiple names, each with a different type. (Contributors of the Microsoft .NET documentation
2021b)
Implementing it in F#
We can implement Song as a sequence of SongInstruction, I have chosen to use sequence as it will
allow F# to choose the data structure which it thinks is best for the situation. I didn’t choose a
specific data structure like an array as we don’t need quick access to any given index at a particular
time.
Simon Roadknight 30
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
We can define SongPlayTime as a type alias for float.
Implementing it in F#
We can define OutputMessage as a type alias for string.
Simon Roadknight 31
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementing it in F#
We can define ErrorMessage as a type alias for string.
Simon Roadknight 32
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
𝑙𝑒𝑡 𝑆𝑜𝑛𝑔𝐸𝑟𝑟𝑜𝑟 = {
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐴𝑟𝑔𝐴𝑚𝑜𝑢𝑛𝑡, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝑆𝑜𝑛𝑔,
𝐼𝑂𝐸𝑟𝑟𝑜𝑟𝑂𝑝𝑒𝑛𝑖𝑛𝑔𝐹𝑖𝑙𝑒, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑒𝑟𝑚𝑖𝑠𝑠𝑖𝑜𝑛,
𝐸𝑚𝑝𝑡𝑦𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒, 𝐹𝑖𝑙𝑒𝑁𝑜𝑡𝐹𝑜𝑢𝑛𝑑
}
Implementing it in F#
We can define it as a discriminated union in F# with each error being a union case.
type SongError =
|InvalidArgAmount
|InvalidFilePath
|InvalidSong
|IOErrorOpeningFile
|InvalidFilePermission
|EmptySongFile
|FileNotFound
Simon Roadknight 33
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
We can then define ExitCode to be the set of all the errors and Success.
𝑆𝑢𝑐𝑐𝑒𝑠𝑠 ∶ 𝑖𝑛𝑡
𝑆𝑢𝑐𝑐𝑒𝑠𝑠 = 0
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐴𝑟𝑔𝐴𝑚𝑜𝑢𝑛𝑡: 𝑖𝑛𝑡
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐴𝑟𝑔𝐴𝑚𝑜𝑢𝑛𝑡 = 1
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ: 𝑖𝑛𝑡
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ = 160
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒: 𝑖𝑛𝑡
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒 = 13
𝐼𝑂𝐸𝑟𝑟𝑜𝑟𝑂𝑝𝑒𝑛𝑖𝑛𝑔𝐹𝑖𝑙𝑒: 𝑖𝑛𝑡
𝐼𝑂𝐸𝑟𝑟𝑜𝑟𝑂𝑝𝑒𝑛𝑖𝑛𝑔𝐹𝑖𝑙𝑒 = 4
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑒𝑟𝑚𝑖𝑠𝑠𝑖𝑜𝑛: 𝑖𝑛𝑡
𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑒𝑟𝑚𝑖𝑠𝑠𝑖𝑜𝑛 = 5
𝐸𝑚𝑝𝑡𝑦𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒: 𝑖𝑛𝑡
𝐸𝑚𝑝𝑡𝑦𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒 = 13
𝐹𝑖𝑙𝑒𝑁𝑜𝑡𝐹𝑜𝑢𝑛𝑑: 𝑖𝑛𝑡
𝐹𝑖𝑙𝑒𝑁𝑜𝑡𝐹𝑜𝑢𝑛𝑑 = 2
𝐸𝑥𝑖𝑡𝐶𝑜𝑑𝑒 ∶ ℙ 𝑖𝑛𝑡
𝐸𝑥𝑖𝑡𝐶𝑜𝑑𝑒 = {𝑆𝑢𝑐𝑐𝑒𝑠𝑠, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐴𝑔𝐴𝑚𝑜𝑢𝑛𝑡, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑎𝑡ℎ, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝑆𝑜𝑛𝑔𝑓𝑖𝑙𝑒,
𝐼𝑂𝐸𝑟𝑟𝑜𝑟𝑂𝑝𝑒𝑛𝑖𝑛𝑔𝐹𝑖𝑙𝑒, 𝐼𝑛𝑣𝑎𝑙𝑖𝑑𝐹𝑖𝑙𝑒𝑃𝑒𝑟𝑚𝑖𝑠𝑠𝑖𝑜𝑛, 𝐸𝑚𝑝𝑡𝑦𝑆𝑜𝑛𝑔𝐹𝑖𝑙𝑒, 𝐹𝑖𝑙𝑒𝑁𝑜𝑡𝐹𝑜𝑢𝑛𝑑}
Implementing it in F#
We can use the Enumeration type in F# to assign labels to specific values. As we know the exit codes
won’t be changing during the execution of the process, we are happy to assign the values at the type
level.
Simon Roadknight 34
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
The first feature is to play a single song file, looking at the Gherkin specifications and data modelling,
these are the initial behaviour steps I came up with.
Simon Roadknight 35
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
We can represent this as a data-transformation graph, where the nodes represent our data types,
and the edges represent functions. This allows us to see how each specific function takes us from
one part of our data model to another. (Cooper, 2022a)
We can also represent this as a task-transition graph where the nodes represent individual functions,
and the edge represent the input / output data. This allows us to see the flow of data in our program
between each function. This can help use model our inputs and outputs for function in our program.
(Cooper, 2022b)
Simon Roadknight 36
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
In F# if we know we are going to have a partial function, here are types we can use to turn the
partial function into a total function, dealing with those items in the source set that do not have a
mapping to the target set. One of these types is the result type (Contributors to the Microsoft .NET
documentation, 2021d), which allows us to return an Ok value if the input has a mapping to the
target set, or an Error if not.
We will use the idea of the Result type when constructing our behaviour steps for playing both a
single and multiple songs.
I have not included the unwrapping of the Result type in my graphs, for this I use Result.bind
unless specified otherwise (FSharp, n.d (a)) Where a graph shows a Result Type as input the
function, but a different type in the maths, assume it has been unwrapped.
𝑠𝑜𝑛𝑔𝑅𝑒𝑠𝑢𝑙𝑡𝑠𝐻𝑎𝑛𝑑𝑙𝑒𝑟: Result < seq Arguments ∗ Result < Song, SongError ≫, SongError >
→ ExitCode
Simon Roadknight 37
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Figure 22 Program
behaviour steps - Task
transition graph
Simon Roadknight 38
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Figure 23 Argument
to FilePath task
transition graph
Simon Roadknight 39
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
We can see the symbol declarations for the partial and total functions for the relationship between
FilePath and SongFile.
Simon Roadknight 40
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 41
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 42
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
We can see the symbol declarations for the partial and total functions for the relationship between
SongFile and Song.
𝑖𝑠𝑆𝑜𝑛𝑔𝑉𝑎𝑙𝑖𝑑: 𝑠𝑒𝑞 𝑆𝑜𝑛𝑔𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑡𝑖𝑜𝑛 𝑜𝑝𝑡𝑖𝑜𝑛 → 𝑅𝑒𝑠𝑢𝑙𝑡 < 𝑠𝑒𝑞 𝑆𝑜𝑛𝑔𝐼𝑛𝑠𝑡𝑟𝑢𝑐𝑡𝑖𝑜𝑛, 𝑆𝑜𝑛𝑔𝐸𝑟𝑟𝑜𝑟 >
Simon Roadknight 43
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Figure 28 SongFile to
Song task transition
graph
Simon Roadknight 44
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
𝑅𝑒𝑠𝑢𝑙𝑡 < 𝑠𝑒𝑞 < 𝐴𝑟𝑔𝑢𝑚𝑒𝑛𝑡 ∗ 𝑅𝑒𝑠𝑢𝑙𝑡 < 𝑆𝑜𝑛𝑔, 𝑆𝑜𝑛𝑔𝐸𝑟𝑟𝑜𝑟 >>, 𝑆𝑜𝑛𝑔𝐸𝑟𝑟𝑜𝑟 > → 𝐸𝑥𝑖𝑡𝐶𝑜𝑑𝑒
Simon Roadknight 45
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 46
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 47
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 48
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 49
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
T1 - Reflection
I am happy with the overall design and planning process of the SongPlayer program. My user stories
are well defined and leave very little room for ambiguity, my data modelling works very well for the
creation of song instructions, and the song itself, however it does fall either side of the construction
of a song or multiple songs, such as the output and error message modelling, this had a knock-on
effect when defining my behaviour steps. My behaviour steps go through all major functionality of
the program, accompanied by data transformation and task transition graphs, as well as function
relations. I would like to improve on the mathematics of the functions, especially the constraining
predicates. Looking forward at the rest of the work completed, the importance of strong data
modelling and feature behaviours has become even more apparent, this is something I will take with
me when I do my final year project as it will most likely be a software development project on a
much larger scale than this.
Simon Roadknight 50
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
T2 - Implementation
We can use the previous defined Gherkin specifications, data modelling, and behaviour steps to start
implementing our programming.
Simon Roadknight 51
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Defining the regular expressions to be used to match Windows and Linux file paths, in this
implementation the program will always match for both, regardless of the Operating System (OS).
Creating the Regex objects with the regular expressions discussed in the data modelling. Defining
two active patterns, one for the Windows file pattern, and one for Linux. I chose to use active
patterns so I could use them in the pattern match expression inside of matchFpPattern. Defining the
function takes in the string and matches it with the active patterns, if there is a match it is a valid file
path, otherwise we return an error.
This is the constructor function for FilePath where we can pass an Argument in and have an Ok
FilePath returned or an Error SongError.
let getFilePath (argument:Input.Argument) = // Argument -> Result<FilePath,
SongError>
argument
|> matchFpPattern
|> Result.bind (FilePath >> Ok)
Figure 36 getFilePath pipeline
We can pass in a FilePath to value, and it will deconstruct is it using pattern matching.
(discriminated_unions). I will refer to these as deconstruction functions throughout the logbook.
let value (FilePath fp) : Input.Argument = fp // FilePath -> Argument
Figure 37 FilePath deconstruction
Simon Roadknight 52
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the Regex objects for the beep, delay, and whitespace patter, and adding the active
patterns for each of them that we can use in our pattern match expressions. I chose to it this way as
it makes the pattern matches later cleaner and readable to anyone viewing the code.
module SongInstructionPatterns =
let beepPattern = Regex("^\s*:beep\s+frequency=(\d+)\s+length=(\d+)ms;?\s*$")
let delayPattern = Regex("^\s*:delay\s+(\d+)ms;?\s*$")
let whitespacePattern = Regex("^\s*$")
let (|BeepPattern|_|) (str:string) =
match (beepPattern.IsMatch str) with
|true -> Some(str)
|false -> None
I have created two functions for extracting the data from the regular expressions, one for the beeps,
and one for the delays. To extract the correct data from the Beeps and Delays I have chosen to use
capture groups, which when a pattern is matched, such as the beep pattern, it’ll also return the data
in the capture groups, which for a Beep would be the Frequency and TimeLength.
let extractBeepValues (str:string) = // string -> string * string
let groupValues = beepPattern.Match(str).Groups
groupValues.Item(1).Value, groupValues.Item(2).Value
Simon Roadknight 53
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the functions outlined in the behaviour steps for the relationship of FilePath to SongFile.
1. We check that the file exists, returning an Ok string if it does or an Error FileNotFound if not
2. We read the contents of the file using F# built in file I/O, I chose to use this route as it opens
and closes the file, and from the example songs, the songs are short enough to not worry
about memory concerns. When performing I/O there are an array of things that can go
wrong, I have decided to catch any exceptions, and instead return a useful error to the user.
3. We check to see if the file we read in was empty, if it was, we return an Error EmptySongFile,
otherwise we return an Ok seq<string>
4. We then check each string in the sequence to see if it matches the song patters we
previously defined, if all of the strings have a match then we return an Ok seq<string>
otherwise we return an Error InvalidSong
Simon Roadknight 54
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
let private isValidSongFile (file:seq<string>) = // seq<string> ->
Result<seq<string>, SongError>
if file |> Seq.forall isValidSongInstruction
then file |> Ok
else Error SongError. InvalidSong
Figure 41 Functions that make up the getSongFile pipeline
Deconstruction function for SongFile. Used to extract seq<string> from the SongFile.
let value (SongFile sf) = sf // SongFile -> seq<string>
Figure 43 SongFile destructor
Simon Roadknight 55
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
1. We define the minFrequency as 37 and maxFrequency 19000 to set up the constraints for
our Frequency when it is being constructed. The minFrequency has slightly changed from
our initial data modelling, this is due to constraints on Console.Beep that specified anything
below 37 will throw an exception.
2. Create the function that takes in an int and return a Some int if the int is in the range
specified, otherwise it returns a None. I have chosen to use the Option type to represent if a
Frequency is valid or not, this was decided when I chose what errors would be returned to
the user, and they didn’t go as low as describing what instruction or part of the instruction
was invalid.
Constructor function for Frequency. This returns Some Frequency if it is valid, or a None if it isn’t.
let constructFrequency frequency = // int -> Frequency option
frequency
|> isValidFrequency
|> Option.bind (Frequency >> Some)
Figure 46 Pipeline for constructing a Frequency
Simon Roadknight 56
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the TimeLength type. During implementation I changed the type of TimeLength from uint to
int, as it is used in Console.Beep like Frequency, as well as Threading.Sleep.
module TimeLength =
type TimeLength = private TimeLength of int
Figure 48 TimeLength module and type
1. We define the timeLengthMin as 50 and the timeLengthMax as decided upon in the data
modelling stages.
2. We check that the int being used to construct the TimeLength is within range.
let private timeLengthMin = 50
let private timeLengthMax = 5000
let private isValidLength timeLength = // int -> int option
if timeLength >= timeLengthMin && timeLength <= timeLengthMax
then Some timeLength
else None
Figure 49 Defining the min and max rangefor TimeLength and creating a function to check whether a value is in range
Simon Roadknight 57
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the module for Beep and the type which is a single discriminated union case.
module Beep =
type Beep = private Beep of Frequency.Frequency * TimeLength.TimeLength
Figure 52 Beep Module and type
We can use the constructBeep function to create a Beep, it takes in a pair of strings. It will then:
Simon Roadknight 58
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
module Delay =
type Delay = private Delay of TimeLength.TimeLength
Figure 55 Delay module and type
The constructDelay function take in a single string as an argument, it then tries to parse it to an
Int32, if it can parse successfully, we will try to construct a TimeLength. We then either return Some
Delay or None.
let constructDelay (timeLength:string) = // string -> Delay option
let (parsed, x) = timeLength |> System.Int32.TryParse
if parsed
then TimeLength.constructTimeLength x
|> function
|Some timeLength -> Delay(timeLength) |> Some
|None -> None
else None
Figure 56 Function for constructing a Delay
Simon Roadknight 59
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the SongInstruction discriminated union. In our data modelling we have the set of
SongInstruction that contains both Beep and Delay types, luckily for us F# gives us an easy way to do
that in the form of discriminated unions, which allows us to have values that can be of more than
one type.
module SongInstruction =
type SongInstruction = private |BeepInstruction of Beep.Beep
|DelayInstruction of Delay.Delay
Figure 58 SongInstruction module and type
The constructSongInstruction function has the source set of string and the target set of
SongInstruction Option. The function will perform a pattern match on the string using the song file
patterns defined earlier, if it matches a Beep or Delay instruction pattern, it will call the
constructBeepInstruction or constructDelayInstruction respectively, the return value of the function
is Some SongInstruction or None.
Simon Roadknight 60
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementation: Song
Creating the Song type which is a single discriminated union case.
module Song =
type Song = private Song of seq<SongInstruction.SongInstruction>
Figure 60 Song module and type
Once we have a valid SongFile, we can pass it into get song which will do the follow:
Figure
let 61 getSong pipeline
getSong (songFile:SongFile.SongFile) = // SongFile -> Result<Song, SongError>
songFile
|> SongFile.value
|> removeWhitespace
|> getSongInstructionsOpts
|> isSongValid
|> Result.bind isEmpty
|> Result.bind (Song >> Ok)
Simon Roadknight 61
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Implementation: Errors
Creating the module for SongError and the type SongError, which contains union cases for any of the
errors that can occur during the program. This allows us to create a particular Error of SongError,
and then use that SongError in pattern matching to get an error message or exit code.
module SongError =
type SongError =
|InvalidArgAmount
|InvalidFilePath
| InvalidSong
|IOErrorOpeningFile
|InvalidFilePermission
|EmptySongFile
|FileNotFound
Figure 63 SongError module and defining the SongError type
The getErrorMessage function takes in a SongError as its source set, and return a string, which is an
error message relating to the input SongError.
let getErrorMessage =
function
|InvalidArgAmount -> "ERROR: No input arguments specified. Try specifying a
song file (e.g., C:\users\anexampleuser\songfiles\song1.txt)."
|InvalidFilePath -> "ERROR: Invalid file path (Please check the file path
was entered correctly and try again)."
| InvalidSong -> "ERROR: Invalid song instruction found in file (e.g.,
invalid :beep or :delay instruction)."
|IOErrorOpeningFile -> "ERROR: IO Error when trying to open the file."
|InvalidFilePermission -> "ERROR: Invalid file permissions (Please check the
file permissions for the file and try again)."
|EmptySongFile -> "ERROR: File contains no song instructions (Please check
the correct file path was specified)."
|FileNotFound -> "ERROR: File not found (Please check the file path was
entered correctly)."
Figure 64 Mapping each SongError to a string with a meaningful message for the error
Simon Roadknight 62
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
module ErrorMessageHandling =
type ErrorMessage = string
Figure 65 ErrorMessageHandling module and type
Creating the module for ExitCode, and an enumeration type for ExitCode . We can have labels for
our exit codes that we decided upon in our Gherkin syntax.
module ExitCode =
type ExitCode = Success = 0 | InvalidArgAmount = 1 | InvalidFilePath = 160
| InvalidSong = 13 |IOErrorOpeningFile = 4
| InvalidFilePermission = 5 | EmptySongFile = 13
| FileNotFound = 2
Figure 67 ExitCode module and enum type
If we have an error, we want to be able to get the correct exit code, we are able use pattern
matching to match the SongError and return an ExitCode. This is a total function, each SongError has
a mapping to an ExitCode.
// SongError -> ExitCode
let getSongErrorExitCode (songError:SongError.SongError) =
match songError with
|SongError.InvalidArgAmount -> ExitCode.InvalidArgAmount
|SongError.InvalidFilePath -> ExitCode.InvalidFilePath
|SongError. InvalidSong -> ExitCode. InvalidSong
|SongError.IOErrorOpeningFile -> ExitCode.IOErrorOpeningFile
|SongError.InvalidFilePermission -> ExitCode.InvalidFilePermission
|SongError.EmptySongFile -> ExitCode.EmptySongFile
|SongError.FileNotFound -> ExitCode.FileNotFound
Figure 68 Mapping each SongError to an ExitCode
Finally, we need a way to be able to get the associated int from our Enum type.
let value (e:ExitCode) = int e // ExitCode -> int
Figure 69 getting int from ExitCodeFigure 70 SongPlayTime type
Simon Roadknight 63
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
1. Getting the seq<SongInstruction> from a Song using the value function in the Song module.
2. Getting each individual instructions play time by using the map function and applying the
getSongInstructionPlayTime function on each element in the seq<SongInstruction>
a. This will perform a pattern match on the SongInstruction and then call the
getBeepPlayTime and getDelayPlayTime depending on what type of instruction it is.
3. We have a sequence of int, but to display the play time of the songs as per the Gherkin
specification and data modelling we want to convert to a float
4. We then use the reduce function on the sequence of int with the (+) operation to get the
total play of the song in milliseconds
5. We convert milliseconds to seconds by dividing by 1000.
6. We then round to two decimal places. (If this number is 56.085 it will round to the nearest
even number, -> 56.08.)
Simon Roadknight 64
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Constructor function for SongPlayTime. As we are dealing with a song as input for the SongPlayTime,
we know what we are dealing with and shouldn’t run into any errors. However, should something
change in the way Song is constructed, it could impact getPlayTimeInSeconds.
let getPlayTimeInSeconds (song:Song) : SongPlayTime = // Song -> SongPlayTime
song
|> value
|> getSongInstructionsPlayTime
|> convertToFloats
|> getPlayTimeInMs
|> convertMsToS
|> roundToTwoDig
Figure 72 Pipeline for getPlayTimeInSeconds
Simon Roadknight 65
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
With our constructed song we want to be able to play it. The first step in this is defining a function
that takes in a song and gets the sequence of SongInstruction that it contains.
Using playSongInstructions with the source set of the seq<SongInstruction> we can iterate over each
instruction using Seq.iter and play that instruction.
The playBeep function takes in a Beep and returns a unit, it extracts the Frequency and TimeLength
from the Beep, and then we get the int from Frequency and TimeLength, and pass it into
Console.Beep.
To play a delay instruction we can extract the TimeLength value from the Delay, and then extract the
int from TimeLength, and pass it into Thread.Sleep.
Simon Roadknight 66
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Creating the module for OutputMessageHandling, and type alias of OutputMessage for string.
module OutputMessageHandling =
type OutputMessage = string
Figure 78 OutputMessageHandling module
This generates the initial output message displaying how many songs are playing, and the total
length of all songs to be played, we can calculate this by creating a list of all the individual song play
times and using the reduce function to add them all together.
// seq<Argument * Song> -> OutputMessage
let successOutput (successSongs:seq<Input.Argument * Song.Song>) : OutputMessage =
let numberOfSongs =
successSongs |> Seq.length
let totalPlayTime =
successSongs
|> Seq.map snd
|> Seq.map Song.getPlayTimeInSeconds
|> Seq.reduce (+)
sprintf "Now playing %i song(s) with a total play time of %gs." numberOfSongs totalPlayTime
Figure 79 Generating a success output to display the number of songs to be played and the play time of all songs combined
Simon Roadknight 67
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
For the SongPlayer program I have tried to use the concepts from Railway Oriented Programming
(Wlaschin, n.d) handling the errors at the end, instead of splitting off and throwing exceptions where
they may crop up.
We can see the main entry point follows the behaviour steps we agreed upon.
[<EntryPoint>]
let main (argv:Input.Arguments) = // string[] -> int
argv
|> checkArgAmount
|> songResultsMapper
|> songResultsHandler
|> ExitCode.value
Figure 80 Entry point of the SongPlayer program
songResultsMapper take in the result we got from checkArgAmount, and then passes that into
Result.Map along with the getSongResults function.
let songResultsMapper (args:Result<Input.Arguments, SongError.SongError>) =
let returnVal = Result.map getSongResults args
returnVal
Figure 82 Result mapper
Simon Roadknight 68
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Finally, it returns the Argument passed in, along with the result of it being passed through the
pipeline.
arg, songResult
Figure 83 Pipeline to transfrom an arg to a Song
Simon Roadknight 69
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
First it will check if there was an Ok result for the number of arguments, if there wasn’t it will return
an Error InvalidArgAmount, otherwise it will then create two collections, one for any successful
songs that have been parsed, and one for any songs that were not successfully parsed. If there were
unsuccessful songs, we generate the ErrorMessage for each one and output it to the stderr, and
then we get the ExitCode for the first error in the sequence. If all songs were parsed successfully, we
generate the output message containing the number of songs to be played, and the play time of all
the songs and print it to the stdout. We then play each song in order, printing to the stdout each
time a new song begins, and a message letting the user know when all songs have played.
Simon Roadknight 70
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
let getSuccessSongs x =
x
|> Seq.choose (fun songResult ->
match songResult with
|(arg, Result.Ok ok) -> Some (arg, ok)
|(arg, Result.Error _) -> None)
let getFailureSongs x =
x
|> Seq.choose (fun songResult ->
match songResult with
|(arg, Result.Ok _) -> None
|(arg, Result.Error err) -> Some (arg, err))
Figure 85 returns a sequence of songs that were succesfully parsed.
let playSongs x =
x
|> Seq.iter (fun (argument:Input.Argument, song:Song.Song) -> printfn "Now
playing: %s" argument; Song.playSong song)
Figure 86 Plays all songs that input
T2 - Reflection
I am happy with the implementation in respect to the gherkin specifications, data modelling and
behaviour steps, however when implementing the program, it was much clearer to implement the
data with a better-defined data model, and the behaviour steps which were involved or focused on
those data models. This is shown in the main pipeline of the programs, the steps involved creating a
song and the creation of the song itself, are clear and concise, whereas the implementation
surrounding that could be better formed, for example the songResultsHandler and the initial data
that is passed into it.
Simon Roadknight 71
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
T3 – Testing
In this section we will be testing our implementation from the perspectives of a user, architect, and
coder so that we can make sure our implementation is fit for purpose, as described in the Gherkin
specifications, it should not encounter errors that will make it halt or crash, and that each piece of
code is doing what we expect.
This is important to do before we let a user the program, for usability, and security concerns. Going
through this process can limit any errors or problems that the program may have.
We will be using a variety of testing methods to accomplish this, from the domains of manual and
automated testing.
Manual testing
What is manual testing?
For our initial testing of the program, we will be using manual testing which focuses on the features
and behaviours from the user’s perspective and interacting with the program itself. Manual testing
allows us to do this by creating a set of test cases with defined input, execution steps, and output
(https://www.guru99.com/test-case.html). We can then use the program following the set of
manual tests to see:
Simon Roadknight 72
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Table 3 Table of input files for manual testing
Simon Roadknight 73
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Below are the set of test cases, each containing an ID, the actions being carried, the input to be able
to carry out these actions, the expected output and expected exit code.
TestID Manual test being carried Input to program Expected output to the Exp.
out user Exit
Code
1 Execute the song player C:\users\anexampleus Now playing 1 song(s) with a 0
program using a valid path er\songfiles\song1.txt total play time of 56.08s.
argument to a single valid Now playing:
song file C:\users\anexampleuser\so
ngfiles\song1.txt
Finished playing all songs.
2 Execute the song player No input ERROR: No input arguments 1
program without specifying specified. Try specifying a
a file path song file (e.g.,
C:\users\anexampleuser\so
ngfiles\song1.txt).
3 Execute the song player C:\users\anexampleus Error in playing song(s). 1 13
program using a valid file er\songfiles\invalidson song(s) with an error found:
path argument to a single g1.txt C:\users\anexampleuser\so
invalid song file ngfiles\invalidsong1.txt –
ERROR: Invalid song
instruction found in file.
(e.g., invalid :beep or :delay
instruction)
Simon Roadknight 74
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Simon Roadknight 75
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 1
Simon Roadknight 76
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 2
Simon Roadknight 77
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 3
Test 4
Simon Roadknight 78
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 5
Simon Roadknight 79
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 6
Test 7
Simon Roadknight 80
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 8
Simon Roadknight 81
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 9
Test 10
Simon Roadknight 82
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 11
Simon Roadknight 83
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Test 12
Simon Roadknight 84
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Automated testing
What is automated testing?
Automated tests let us test our code and program programmatically, that is to write pieces of
software that we can execute to test our program (https://www.guru99.com/automation-
testing.html), be those specific functions, larger parts of the program, or the whole program. We
want to be able to run many tests at once with varying input and output, automated testing allows
for this in a way that manual testing does not.
1. If we have automated tests written for a specific function, if we were to make changes to
that function, we would be able to run the already written test to make sure that we haven’t
broken the function, and that the test case still passes.
2. To be able to test as many scenarios as possible at one time, that is to be able to provide
many inputs to expected outputs and run them all at once.
3. We can use them while developing our software, we can choose to write tests before hand
and then write our program, when we have completed individual functions, test for those
functions should now pass instead of fail. This is known as Test-Driven Development
(https://www.guru99.com/test-driven-development.html).
Like manual testing, this allows us to confirm our program is working as intended, however, on a
much larger scale and at a more granular level. As we can test specific functions, we are able to look
back at our data modelling, behaviour steps and function specifications, and see any insufficiencies
in our execution, or iterate on what we previously thought was the intended goal.
1. Unit-testing allows us to test specific functions, and larger parts of our code by supplying an
input and expected out.
2. Property-based testing will also allow us to test specific functions, and larger parts of our
code, instead of providing it with specific input, we are able to generate the input based on
the properties it has, meaning instead of only testing on a few specific values we can
potentially test on thousands and millions.
3. Automated behavioural tests, these will be based on our gherkin specifications agreed upon
earlier. It will allow us to carry out tests such as our manual test suite, but in an automated
way.
Simon Roadknight 85
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Unit-testing
The first kind of automated testing we will use is unit testing, this is taking parts of our code and
testing them individually, we will be applying parts of the code to a known input, with a known
expected output. This will let us know if any individual parts of the code are not working as intended.
To carry out the unit tests we will look back at our behaviour steps, and function definitions, and
pick out small enough functions that when we do run the tests, should they fail it will be a small
enough piece of code to be able to debug and retest.
With that in mind I have decided to unit test the following functionality as each of these pieces are
self-contained enough should they not pass any unit tests written for them, I will have sufficient
information to start debugging the issue.
1. Constructing a beep
2. Constructing a delay
3. Constructing a file path (Validating the file path can be a file path)
4. Constructing a song file
5. Construct song
6. Get song play time
Simon Roadknight 86
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
When trying to unit-test with I ran into issues with testing as my types were private. I was able to
slowly extract the values, but this didn’t seem like the right way to go. What I wanted to do was be
able to match on something like this:
However, as I don’t have access to the Beep, Frequency or TimeLength from my unit test it didn’t
appear I could do that, I believe I could have written some helper code in the modules, but I wasn’t
sure whether it was best practice to do so.
Simon Roadknight 87
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Property-bad testing
Property based testing is similar to unit testing, in that we are testing particular functions with
known inputs and expected outputs, however, unlike the unit-testing we have previously done, we
don’t have to define specific inputs for our tests, instead we can provide specification in the form of
properties of which our functions should satisfy, we can then test that property holds when running
a large number of randomly generate test cases.
To carry out BDD style testing, there are frameworks available for F# and .NET as a whole such as
TickSpec, these allow us to the write the tests in the Given, When, Then format that we used for our
Gherkin specification. Allowing for us to use the specifications decided upon previously as the basis
for our testing.
T1 - Reflection
For the testing part of the logbook, I am happy with manual testing carried out, and my
understanding of what testing is, why it is used, and the different types of testing. I am happy with
the test suites I have come up with both the manual tests and the unit tests. Going forward I will
have the consideration of testing when writing my code, making it so that my code is easier to test
with the techniques that I have learned about.
Simon Roadknight 88
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
I have chosen to look at the F# Koans exercise project. It is not necessarily a project that has been
written and documented for other programmers to work on and contribute towards, a lot of the
documentation lends itself to how to use the program, which is a great place to start with any
project you want to contribute to, understanding how it works from the user’s perspective. On
project which are actively looking for contributors existing programming documentation can possibly
help the project in a few ways
1. Increase the quality of code that is being written, especially if the documentation includes defined style
guides.
2. Improve the quality of commit messages is there is a standardised style.
3. Reduce overhead of the same questions being asked
4. Allows people to onboard to the project quicker if there is documentation which explains any API’s the
project may contain.
5. Increase the likelihood of having people want to contribute to the project.
The F# Koans uses version control in the form of Git. There are other version control systems, but
fundamentally they are used to keep source code in a repository that can keep track of the changes
that developers make to the source code and keeping a history of previous versions. We can see that
from the F# Koans project, multiple people are able to contribute to the same project, be it
contributing code, fixing typing mistakes, adding documentation, or requestion a feature.
We will look at some of the commands that allow for multiple users to work on the same project.
Simon Roadknight 89
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
to integrate one repository with another. (Git, n.d (c)). I could have left it
some time since adding my new topic, and realised I am behind the
remote repository, I could pull the changes and have it integrated with
my local version of the F# Koans. If multiple people are working on a
project, someone could finish one part of the code, push it to the
repository, and another person could then pull that code, integrate it
with their own and use the newly pulled parts of the project should they
need to.
Push You are finally ready to share the work that you have put into
implementing that new feature, or fixing an issue, here is where you are
able to push your local repository to the remote repository (Atlassian,
n.d). I am now able to use push to send my changes to the remote
repository, where people can enjoy the new topic, I added. This allows
people to share the changes they have made, which allows for
collaboration.
Merge Is taking two branches and putting them together – Git has its own
algorithm for doing this, what it does is try to find the base commit
where both branches match, then merges on that if there are no
conflicts, if there are conflicts, the process can be done manually. This
could be the case if someone had the idea to add the same topic as me to
the F# Koans project with an identical name, there will be a merge
conflict. It is very inevitable that two people will eventually alter the
same piece of code, or a piece of code someone else relying on for their
commit, this can be managed either by Git itself if the problem is trivial,
or often by a senior member of staff should it be more complex
Branch Branch allows you to create a separate version of the project that differs
from the main repository. The use case for this can be for different
features that are being worked on, different teams, and different
versions. In a business there may be several features being developed at
once, each feature can have its own branch until it is ready to be merged,
this allows for collaboration on a smaller scale for features, without
interrupting the main branch constantly.
Pull request If you have made changes to a project and you don’t have write access,
you can submit a pull request to ask the maintainer of the Git repository
to review code they want to merge. If I wanted to upload my new F#
Koans to the default branch ‘fsharp’ I would have to submit a pull request
and have it reviewed. This is what enables open source without the
threat of any code being pushed to any project, the ability for
contributors to make pull requests to a project, this means all types of
people, from users who want improved documentation, to someone
adding a new feature, or fixing a bug they found. This along with the
other tools allows for a multi-person software development project to be
managed easier than it otherwise may be.
Simon Roadknight 90
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Looking through the commit history of the F# Koans project, I have discovered
‘commit 78aa77df3f48dfb986fa8973b91296205c739cf4’ titles initial rev of F# branch to be the first
major commit in the project. The change was committed by ChrisMarinos. Prior to this commit, the
git repository only had a few files and no code, the commit changed 18 files, with 10,454 additions
and no deletions. It added the initial functionality to the F# Koans as we see it today.
Issue #51 caught my eye as it seemed to span several issues including Issue #15, there was an issue
with NUnit and how ‘AssertEquality’ defines being equal. Issue #15 was from 2012, and Issue #51
which dealt with it was from 2017. The initial issue #15 was raised by ChrisMarinos, the fix/Issue #51
was raised by dsyme, the changed enforced that types are checks before allowing the Koan to be run
from what I can understand.
Pull request #94, made changes so that netcore 3.1 was targeted as it was the new LTS and removed
an older unsupported netcore target. This pull request was made by cartermp in 2020.
Simon Roadknight 91
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Reference List
Knight, A. (2017) BDD 101: Writing Good Gherkin. automationpanda [blog] 30 January. Available at:
https://automationpanda.com/2017/01/30/bdd-101-writing-good-gherkin/ [Accessed 22 May
2022].
Contributors to the Microsoft .NET documentation (2021) Console Applications. Available at:
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/entry-point
[Accessed 23 May 2022].
Contributors to the Microsoft .NET documentation (2022) Regular Expression Language – Quick
Reference. Available at: https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-
expression-language-quick-reference [Accessed 23 May 2022].
Contributors to the Microsoft .NET documentation (2021b) Discriminated Unions. Available at:
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions
[Accessed 21 May 2022].
Contributors to the Microsoft documentation (2021c) System Error Codes (0-499). Available at:
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- [Accessed 21
May 2022].
Cooper, E. (2022a) The Structure of Programs: Some lenses to view software through [Part 5]. [pdf]
Available at:
https://moodle.bcu.ac.uk/pluginfile.php/8531492/mod_resource/content/2/PlanningProgramsWee
k10_1.pdf [Accessed 23 May 2022].
Simon Roadknight 92
21126080
CMP5361 Computer Mathematics and Declarative Programming UG2
Software Creation Tasks
Cooper, E. (2022b) The Structure of Programs: Some lenses to view software through [Part 5]. [pdf]
Available at:
https://moodle.bcu.ac.uk/pluginfile.php/8531492/mod_resource/content/2/PlanningProgramsWee
k10_1.pdf [Accessed 23 May 2022].
Scott Wlaschin, Railway Oriented Programming fsharpforfunandprofit [blog] n.d. Available at:
https://fsharpforfunandprofit.com/rop/ [Accessed 22 May 2022].
Git (n. d (a)) git-clone. Available at: https://git-scm.com/docs/git-clone [Accessed 21 May 2022].
Git (n. d (b)) git-commit. Available at: https://git-scm.com/docs/git-commit [Accessed 21 May 2022].
Git (n. d (c)) git-pull. Available at: https://git-scm.com/docs/git-pull [Accessed 21 May 2022].
Simon Roadknight 93
21126080