Skip to content

Commit

Permalink
feature(error-handling): add error-handling practice exercise
Browse files Browse the repository at this point in the history
  • Loading branch information
ajborla committed May 3, 2024
1 parent 8e362dc commit 88c3abd
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@
"practices": [],
"prerequisites": [],
"difficulty": 3
},
{
"slug": "error-handling",
"name": "Error Handling",
"uuid": "81d7f922-4988-44d7-81bc-7fddc740bdf4",
"practices": [],
"prerequisites": [],
"difficulty": 1
}
]
},
Expand Down
8 changes: 8 additions & 0 deletions exercises/practice/error-handling/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Instructions

Implement various kinds of error handling and resource management.

An important point of programming is how to handle errors and close resources even if errors occur.

This exercise requires you to handle various errors.
Because error handling is rather programming language specific you'll have to refer to the tests for your track to see what's exactly required.
9 changes: 9 additions & 0 deletions exercises/practice/error-handling/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"authors": ["ajborla"],
"files": {
"solution": ["error_handling.prg"],
"test": ["error_handling_test.prg"],
"example": [".meta/example.prg"]
},
"blurb": "Implement various kinds of error handling and resource management."
}
13 changes: 13 additions & 0 deletions exercises/practice/error-handling/.meta/example.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
* ----------------------------------------------------------------------------
* exercism.org
* Harbour Track Exercise: error-handling
* Contributed: Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

function HandleError(name)
* Reject missing, or wrong number of arguments
if PCOUNT() <> 1 ; return NIL ; endif

* Perform expected processing
return "Hello, " + name

153 changes: 153 additions & 0 deletions exercises/practice/error-handling/PRGUNIT.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
* ----------------------------------------------------------------------------
* Harbour Unit Test Worker
* Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

#ifndef UTILS_PRG
#include "utils.prg"
#endif

procedure MakeTestDatabaseStructure(dbfName)
* Creation overwites any existing database
create &dbfName

* Each record describes the structure of a FIELD
append blank
replace Field_name with "NAME", Field_type with "C",;
Field_Len with 80, Field_dec with 0
append blank
replace Field_name with "CMPOP", Field_type with "C",;
Field_Len with 2, Field_dec with 0
append blank
replace Field_name with "EXPVALUE", Field_type with "C",;
Field_Len with 80, Field_dec with 0
append blank
replace Field_name with "CMDSTR", Field_type with "C",;
Field_Len with 80, Field_dec with 0

* Ensure data written to disk
close &dbfName
return

procedure MakeTestDatabase(dbfName)
local dbfStructure := dbfName + "_STRUCTURE"

* Build test database from database structure file
do MakeTestDatabaseStructure with dbfStructure
create &dbfName from &dbfStructure

* Ensure database structure file is removed
dbfStructure := dbfStructure + ".dbf"
erase &dbfStructure
return

procedure AddTestDatabase(dbfName, testName, cmpOp, expValue, cmdStr)
* Load a test data record into tests database (note use of 'Wrap' to
* preserve spaces in expected value string)
use &dbfName
append blank
replace &dbfName->NAME with testName
replace &dbfName->CMPOP with cmpOp
replace &dbfName->EXPVALUE with Wrap(expValue)
replace &dbfName->CMDSTR with cmdStr
close &dbfName
return

function RunTests(dbfName, keepTestDBF, outputJSON)
local testName, cmpOp, expValue, cmdStr, retValue, testExpr
local success := .T.

use &dbfName

* Determine, and print, number of tests (required for TAP)
if outputJSON == NIL .OR. !outputJSON
? "1.." + LTRIM(STR(LASTREC()))
endif

* Execute unit tests
do while !EOF()
* Extract test data (note use of 'Unwrap' to extract space-preserved
* expected value string)
testName := ALLTRIM(&dbfName->NAME)
cmpOp := &dbfName->CMPOP
expValue := Unwrap(ALLTRIM(&dbfName->EXPVALUE))
cmdStr := ALLTRIM(&dbfName->CMDSTR)

* Execute test, and build test expression
retValue := TypeToS(&cmdStr)
testExpr := '"' + retValue + '" ' + cmpOp + ' "' + expValue + '"'

* If the parameter flag, outputJSON, is omitted, or set to .F., then
* emit test report in TAP format
if outputJSON == NIL .OR. !outputJSON
* Report test outcome - TAP
if &testExpr
? "OK " + LTRIM(STR(RECNO())) + " - " + testName
else
* Single test failure signals failure of whole suite
success := .F.
? "FAIL " + LTRIM(STR(RECNO())) + " - " + testName
endif
else
* Report test outcome - JSON
? "JSON"
endif

* ... next test
skip
enddo

close &dbfName

* If the parameter flag, keepTestDBF, is omitted, or set to .F., then
* remove the tests database
if keepTestDBF == NIL .OR. !keepTestDBF
dbfName := dbfName + ".dbf"
erase &dbfName
endif

return success

function TypeToS(value)
* Use VALTYPE() instead of TYPE() to check type
local typeValue := VALTYPE(value)

switch typeValue
* Array type (assume 1D array of non-aggregate elements),
* returns the concatenation of elements as a string
case "A" ; return ArrToS(value)

* Character type returned untouched
case "C" ; return value

* Date as "yyyymmdd"
case "D" ; return DTOS(value)

* Logical as literal string representation of self
case "L" ; return IIF(value, ".T.", ".F.")

* String-converted numerics are right-justified, so ensure are
* returned trimmed
case "N" ; return ALLTRIM(STR(value))

* Support use of NIL return type (usually to indicate error)
case "U" ; return "NIL"
endswitch

* Ignore the remaining types, just return NIL (likely runtime error)
return NIL

* Utilities to preserve leading and trailing spaces in strings as they
* are stored into, and extracted from, database fields
function Wrap(string) ; return WrapString(string, .F., "[", "]")
function Unwrap(string) ; return WrapString(string, .T.)

function WrapString(string, doUnwrap, wrapStart, wrapEnd)
local uws
if doUnwrap
uws := SUBSTR(SUBSTR(string, 2), 1, LEN(string) - 2)
else
uws := wrapStart + string + wrapEnd
endif
return uws

8 changes: 8 additions & 0 deletions exercises/practice/error-handling/error_handling.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* ----------------------------------------------------------------------------
* exercism.org
* Harbour Track Exercise: error-handling
* ----------------------------------------------------------------------------

function HandleError(name)
return ""

47 changes: 47 additions & 0 deletions exercises/practice/error-handling/error_handling_test.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
* ----------------------------------------------------------------------------
* Harbour Unit Test Runner [error_handling.prg]
* ----------------------------------------------------------------------------

* Variable declarations
memvar TESTS, SUCCESS

* Test database name
TESTS := IIF(PCOUNT() > 0, hb_PValue(1), "TESTS")

* Create tests database
do MakeTestDatabase with TESTS

* Add test data into tests database. Each test case stub should be altered
* to follow this format:
*
* do AddTestDatabase with TESTS, "say Hi!", "==", "Hello, World!", "HelloWorld()"
* do AddTestDatabase with TESTS, "add 5 and 3", "==", "8", "Add_5_And_3(5, 3)"
*
* Note:
* 1st field is the test description (already supplied)
* 2nd field is comparator operator, usually "=="
* 3rd field is the function return result, always written as a string
* 4th field is the function (optionally with arguments), always written as a string
*

* Add test data into tests database
do AddTestDatabase with TESTS, "Correct arguments", "==", "Hello, Alice", "HandleError('Alice')"
do AddTestDatabase with TESTS, "One long argument", "==", "Hello, Alice and Bob", "HandleError('Alice and Bob')"
do AddTestDatabase with TESTS, "Incorrect arguments", "==", "NIL", "HandleError('Alice', 'Bob')"
do AddTestDatabase with TESTS, "Return error indicator with no value given", "==", "NIL", "HandleError()"
do AddTestDatabase with TESTS, "Empty argument", "==", "Hello, ", "HandleError('')"

* Execute unit tests. Arguments:
* - Tests database name
* - Database retention flag (.T. to not delete test database on test end)
* - JSON output flag (.T. to emit test results in JSON format [default is TAP])
SUCCESS := RunTests(TESTS, SToBool(hb_PValue(2)), SToBool(hb_PValue(3)))

* Return success status to OS
ERRORLEVEL(IIF(SUCCESS, 0, 1))

* Code under test (CUT)
#include "error_handling.prg"

* Unit Test Framework
#include "PRGUNIT.prg"
86 changes: 86 additions & 0 deletions exercises/practice/error-handling/utils.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
* ----------------------------------------------------------------------------
* Harbour Miscellaneous Utilities
* Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

#define UTILS_PRG

*
* Given a string, charSet, interpreted as a set of individual characters,
* and a string, string, returns a copy of string with all occurrences of
* the characters in charSet, removed.
*
function RemoveCharSet(charSet, string)
local i, clen := LEN(charSet)
for i := 1 to clen
string := STRTRAN(string, SUBSTR(charSet, i, 1))
next
return string

*
* Given a string, and a separator string (usually a single character)
* returns an array of separator-split tokens, or the original string
* if separation not possible.
*
function SToArr(string, separator)
local array := {}, i, element

* Return untouched string if no separator, or it is not in string
if PCOUNT() < 2 .OR. separator == NIL ; return string ; endif
i := AT(separator, string) ; if i == 0 ; return string ; endif

* Parse the string, extracting each element, and adding to array
do while i <> 0
element := LEFT(string, i - 1)
if !EMPTY(element) ; AADD(array, element) ; endif
string := SUBSTR(string, i + 1)
i := AT(separator, string)
enddo

* Handle last element, and return array
if !EMPTY(string) ; AADD(array, string) ; endif
return array

*
* Given an array whose elements are non-aggregate types, returns a
* string of those elements separated by separator. If a string
* cannot be built, an empty string is returned.
*
function ArrToS(array, separator)
local i, element, string := "", arrlen := LEN(array)
if PCOUNT() < 2 .OR. separator == NIL ; separator := "" ; endif
if arrlen < 1 ; return "" ; endif
for i := 1 to arrlen
element := IIF(VALTYPE(array[i]) <> "C", ALLTRIM(STR(array[i])), array[i])
string += element + separator
next
return ;
IIF(EMPTY(separator), string, SUBSTR(string, 1, RAT(separator, string) - 1))

*
* Given a string, returns the Boolean status indicating whether
* it is convertible to an integer. Non-numeric and floating
* point values will both return .F.
*
function IsINTString(s)
local slen, i
if PCOUNT() <> 1 .OR. VALTYPE(s) <> "C" ; return .F. ; endif
slen := LEN(s)
if AT(".", s) <> 0 .OR. slen < 1 ; return .F. ; endif
if VAL(s) <> 0 ; return .T. ; endif
for i := 1 to slen
if !(SUBSTR(s, i, 1) $ "0123456789") ; return .F. ; endif
next
return .T.

*
* Given a string, returns the Boolean value represented.
*
function SToBool(s)
return ;
IIF(VALTYPE(s) <> "C", NIL, ;
IIF(UPPER(s) == ".T.", .T., ;
IIF(UPPER(s) == ".F.", .F., ;
IIF(SUBSTR(s, 1, 1) $ 'Tt', .T., ;
IIF(SUBSTR(s, 1, 1) $ 'Ff', .F., NIL)))))

0 comments on commit 88c3abd

Please sign in to comment.