From 88c3abd0d9203d7c37fa34cc57a95a7f228be5e8 Mon Sep 17 00:00:00 2001 From: ajborla Date: Sat, 4 May 2024 07:46:13 +1000 Subject: [PATCH] feature(error-handling): add error-handling practice exercise --- config.json | 8 + .../error-handling/.docs/instructions.md | 8 + .../practice/error-handling/.meta/config.json | 9 ++ .../practice/error-handling/.meta/example.prg | 13 ++ exercises/practice/error-handling/PRGUNIT.prg | 153 ++++++++++++++++++ .../error-handling/error_handling.prg | 8 + .../error-handling/error_handling_test.prg | 47 ++++++ exercises/practice/error-handling/utils.prg | 86 ++++++++++ 8 files changed, 332 insertions(+) create mode 100644 exercises/practice/error-handling/.docs/instructions.md create mode 100644 exercises/practice/error-handling/.meta/config.json create mode 100644 exercises/practice/error-handling/.meta/example.prg create mode 100644 exercises/practice/error-handling/PRGUNIT.prg create mode 100644 exercises/practice/error-handling/error_handling.prg create mode 100644 exercises/practice/error-handling/error_handling_test.prg create mode 100644 exercises/practice/error-handling/utils.prg diff --git a/config.json b/config.json index ea89c08..a6dbc3c 100644 --- a/config.json +++ b/config.json @@ -73,6 +73,14 @@ "practices": [], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "error-handling", + "name": "Error Handling", + "uuid": "81d7f922-4988-44d7-81bc-7fddc740bdf4", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ] }, diff --git a/exercises/practice/error-handling/.docs/instructions.md b/exercises/practice/error-handling/.docs/instructions.md new file mode 100644 index 0000000..25dd4d2 --- /dev/null +++ b/exercises/practice/error-handling/.docs/instructions.md @@ -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. diff --git a/exercises/practice/error-handling/.meta/config.json b/exercises/practice/error-handling/.meta/config.json new file mode 100644 index 0000000..3050ef9 --- /dev/null +++ b/exercises/practice/error-handling/.meta/config.json @@ -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." +} diff --git a/exercises/practice/error-handling/.meta/example.prg b/exercises/practice/error-handling/.meta/example.prg new file mode 100644 index 0000000..e2f8107 --- /dev/null +++ b/exercises/practice/error-handling/.meta/example.prg @@ -0,0 +1,13 @@ +* ---------------------------------------------------------------------------- +* exercism.org +* Harbour Track Exercise: error-handling +* Contributed: Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +function HandleError(name) + * Reject missing, or wrong number of arguments + if PCOUNT() <> 1 ; return NIL ; endif + +* Perform expected processing +return "Hello, " + name + diff --git a/exercises/practice/error-handling/PRGUNIT.prg b/exercises/practice/error-handling/PRGUNIT.prg new file mode 100644 index 0000000..99d8c4e --- /dev/null +++ b/exercises/practice/error-handling/PRGUNIT.prg @@ -0,0 +1,153 @@ +* ---------------------------------------------------------------------------- +* Harbour Unit Test Worker +* Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +#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 + diff --git a/exercises/practice/error-handling/error_handling.prg b/exercises/practice/error-handling/error_handling.prg new file mode 100644 index 0000000..d8aaa93 --- /dev/null +++ b/exercises/practice/error-handling/error_handling.prg @@ -0,0 +1,8 @@ +* ---------------------------------------------------------------------------- +* exercism.org +* Harbour Track Exercise: error-handling +* ---------------------------------------------------------------------------- + +function HandleError(name) +return "" + diff --git a/exercises/practice/error-handling/error_handling_test.prg b/exercises/practice/error-handling/error_handling_test.prg new file mode 100644 index 0000000..49fa8c5 --- /dev/null +++ b/exercises/practice/error-handling/error_handling_test.prg @@ -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" diff --git a/exercises/practice/error-handling/utils.prg b/exercises/practice/error-handling/utils.prg new file mode 100644 index 0000000..a6275f9 --- /dev/null +++ b/exercises/practice/error-handling/utils.prg @@ -0,0 +1,86 @@ +* ---------------------------------------------------------------------------- +* Harbour Miscellaneous Utilities +* Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +#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))))) +