From ad2cabcce54d764cd03fba79906c9fdba728287e Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Wed, 26 Feb 2025 01:42:51 -0500 Subject: [PATCH 1/6] Update unittest module to work with module contexts --- modules.json | 4 +- src/bundles/testing/asserts.ts | 97 ----- src/bundles/testing/list.ts | 372 ------------------ .../{testing => unittest}/__tests__/index.ts | 14 +- src/bundles/unittest/asserts.ts | 105 +++++ .../{testing => unittest}/functions.ts | 37 +- src/bundles/{testing => unittest}/index.ts | 69 ++-- src/bundles/{testing => unittest}/mocks.ts | 2 +- src/bundles/{testing => unittest}/types.ts | 1 + src/tabs/Testing/index.tsx | 128 ------ src/tabs/Unittest/index.tsx | 124 ++++++ 11 files changed, 298 insertions(+), 655 deletions(-) delete mode 100644 src/bundles/testing/asserts.ts delete mode 100644 src/bundles/testing/list.ts rename src/bundles/{testing => unittest}/__tests__/index.ts (76%) create mode 100644 src/bundles/unittest/asserts.ts rename src/bundles/{testing => unittest}/functions.ts (57%) rename src/bundles/{testing => unittest}/index.ts (70%) rename src/bundles/{testing => unittest}/mocks.ts (91%) rename src/bundles/{testing => unittest}/types.ts (97%) delete mode 100644 src/tabs/Testing/index.tsx create mode 100644 src/tabs/Unittest/index.tsx diff --git a/modules.json b/modules.json index 7c16dc4a66..f8a3abf08f 100644 --- a/modules.json +++ b/modules.json @@ -117,9 +117,9 @@ "Nbody" ] }, - "testing": { + "unittest": { "tabs": [ - "Testing" + "Unittest" ] } } \ No newline at end of file diff --git a/src/bundles/testing/asserts.ts b/src/bundles/testing/asserts.ts deleted file mode 100644 index 1d8a49753d..0000000000 --- a/src/bundles/testing/asserts.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { is_pair, head, tail, is_list, is_null, member, length } from './list'; - -/** - * Asserts that a predicate returns true. - * @param pred An predicate function that returns true/false. - * @returns - */ -export function assert(pred: () => boolean) { - if (!pred()) { - throw new Error('Assert failed!'); - } -} - -/** - * Asserts the equality (===) of two parameters. - * @param expected The expected value. - * @param received The given value. - * @returns - */ -export function assert_equals(expected: any, received: any) { - const fail = () => { - throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); - }; - if (typeof expected !== typeof received) { - fail(); - } - // approx checking for floats - if (typeof expected === 'number' && !Number.isInteger(expected)) { - if (Math.abs(expected - received) > 0.001) { - fail(); - } else { - return; - } - } - if (expected !== received) { - fail(); - } -} - -/** - * Asserts that two parameters are not equal (!==). - * @param expected The expected value. - * @param received The given value. - * @returns - */ -export function assert_not_equals(expected: any, received: any) { - if (expected === received) { - throw new Error(`Expected \`${expected}\` to not equal \`${received}\`!`); - } -} - -/** - * Asserts that `xs` contains `toContain`. - * @param xs The list to assert. - * @param toContain The element that `xs` is expected to contain. - */ -export function assert_contains(xs: any, toContain: any) { - const fail = () => { - throw new Error(`Expected \`${xs}\` to contain \`${toContain}\`.`); - }; - - if (is_null(xs)) { - fail(); - } else if (is_list(xs)) { - if (is_null(member(toContain, xs))) { - fail(); - } - } else if (is_pair(xs)) { - if (head(xs) === toContain || tail(xs) === toContain) { - return; - } - - // check the head, if it fails, checks the tail, if that fails, fail. - try { - assert_contains(head(xs), toContain); - return; - } catch (_) { - try { - assert_contains(tail(xs), toContain); - return; - } catch (__) { - fail(); - } - } - } else { - throw new Error(`First argument must be a list or a pair, got \`${xs}\`.`); - } -} - -/** - * Asserts that the given list has length `len`. - * @param list The list to assert. - * @param len The expected length of the list. - */ -export function assert_length(list: any, len: number) { - assert_equals(length(list), len); -} diff --git a/src/bundles/testing/list.ts b/src/bundles/testing/list.ts deleted file mode 100644 index fdc49241e4..0000000000 --- a/src/bundles/testing/list.ts +++ /dev/null @@ -1,372 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention, no-else-return, prefer-template, no-param-reassign, no-plusplus, operator-assignment, no-lonely-if */ -/* prettier-ignore */ -// list.js: Supporting lists in the Scheme style, using pairs made -// up of two-element JavaScript array (vector) - -// Author: Martin Henz - -// Note: this library is used in the externalLibs of cadet-frontend. -// It is distinct from the LISTS library of Source ยง2, which contains -// primitive and predeclared functions from Chapter 2 of SICP JS. - -// array test works differently for Rhino and -// the Firefox environment (especially Web Console) -export function array_test(x) : boolean { - if (Array.isArray === undefined) { - return x instanceof Array; - } else { - return Array.isArray(x); - } -} - -// pair constructs a pair using a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function pair(x, xs): [any, any] { - return [x, xs]; -} - -// is_pair returns true iff arg is a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_pair(x): boolean { - return array_test(x) && x.length === 2; -} - -// head returns the first component of the given pair, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function head(xs): any { - if (is_pair(xs)) { - return xs[0]; - } else { - throw new Error( - 'head(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// tail returns the second component of the given pair -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function tail(xs) { - if (is_pair(xs)) { - return xs[1]; - } else { - throw new Error( - 'tail(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// is_null returns true if arg is exactly null -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_null(xs) { - return xs === null; -} - -// is_list recurses down the list and checks that it ends with the empty list [] -// does not throw Value exceptions -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_list(xs) { - for (; ; xs = tail(xs)) { - if (is_null(xs)) { - return true; - } else if (!is_pair(xs)) { - return false; - } - } -} - -// list makes a list out of its arguments -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list(...args) { - let the_list: any = null; - for (let i = args.length - 1; i >= 0; i--) { - the_list = pair(args[i], the_list); - } - return the_list; -} - -// list_to_vector returns vector that contains the elements of the argument list -// in the given order. -// list_to_vector throws an exception if the argument is not a list -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list_to_vector(lst) { - const vector: any[] = []; - while (!is_null(lst)) { - vector.push(head(lst)); - lst = tail(lst); - } - return vector; -} - -// vector_to_list returns a list that contains the elements of the argument vector -// in the given order. -// vector_to_list throws an exception if the argument is not a vector -// LOW-LEVEL FUNCTION, NOT SOURCE -export function vector_to_list(vector) { - let result: any = null; - for (let i = vector.length - 1; i >= 0; i = i - 1) { - result = pair(vector[i], result); - } - return result; -} - -// returns the length of a given argument list -// throws an exception if the argument is not a list -export function length(xs) { - let i = 0; - while (!is_null(xs)) { - i += 1; - xs = tail(xs); - } - return i; -} - -// map applies first arg f to the elements of the second argument, -// assumed to be a list. -// f is applied element-by-element: -// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] -// map throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the first -// argument is not a function. -// tslint:disable-next-line:ban-types -export function map(f, xs) { - return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); -} - -// build_list takes a non-negative integer n as first argument, -// and a function fun as second argument. -// build_list returns a list of n elements, that results from -// applying fun to the numbers from 0 to n-1. -// tslint:disable-next-line:ban-types -export function build_list(n, fun) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'build_list(n, fun) expects a positive integer as ' + - 'argument n, but encountered ' + - n - ); - } - - // tslint:disable-next-line:ban-types - function build(i, alreadyBuilt) { - if (i < 0) { - return alreadyBuilt; - } else { - return build(i - 1, pair(fun(i), alreadyBuilt)); - } - } - - return build(n - 1, null); -} - -// for_each applies first arg fun to the elements of the list passed as -// second argument. fun is applied element-by-element: -// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). -// for_each returns true. -// for_each throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the -// first argument is not a function. -// tslint:disable-next-line:ban-types -export function for_each(fun, xs) { - if (!is_list(xs)) { - throw new Error( - 'for_each expects a list as argument xs, but encountered ' + xs - ); - } - for (; !is_null(xs); xs = tail(xs)) { - fun(head(xs)); - } - return true; -} - -// reverse reverses the argument list -// reverse throws an exception if the argument is not a list. -export function reverse(xs) { - if (!is_list(xs)) { - throw new Error( - 'reverse(xs) expects a list as argument xs, but encountered ' + xs - ); - } - let result: any = null; - for (; !is_null(xs); xs = tail(xs)) { - result = pair(head(xs), result); - } - return result; -} - -// append first argument list and second argument list. -// In the result, the [] at the end of the first argument list -// is replaced by the second argument list -// append throws an exception if the first argument is not a list -export function append(xs, ys) { - if (is_null(xs)) { - return ys; - } else { - return pair(head(xs), append(tail(xs), ys)); - } -} - -// member looks for a given first-argument element in a given -// second argument list. It returns the first postfix sublist -// that starts with the given element. It returns [] if the -// element does not occur in the list -export function member(v, xs) { - for (; !is_null(xs); xs = tail(xs)) { - if (head(xs) === v) { - return xs; - } - } - return null; -} - -// removes the first occurrence of a given first-argument element -// in a given second-argument list. Returns the original list -// if there is no occurrence. -export function remove(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return tail(xs); - } else { - return pair(head(xs), remove(v, tail(xs))); - } - } -} - -// Similar to remove. But removes all instances of v instead of just the first -export function remove_all(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return remove_all(v, tail(xs)); - } else { - return pair(head(xs), remove_all(v, tail(xs))); - } - } -} - -// for backwards-compatibility -// equal computes the structural equality -// over its arguments -export function equal(item1, item2) { - if (is_pair(item1) && is_pair(item2)) { - return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); - } else { - return item1 === item2; - } -} - -// assoc treats the second argument as an association, -// a list of (index,value) pairs. -// assoc returns the first (index,value) pair whose -// index equal (using structural equality) to the given -// first argument v. Returns false if there is no such -// pair -export function assoc(v, xs) { - if (is_null(xs)) { - return false; - } else if (equal(v, head(head(xs)))) { - return head(xs); - } else { - return assoc(v, tail(xs)); - } -} - -// filter returns the sublist of elements of given list xs -// for which the given predicate function returns true. -// tslint:disable-next-line:ban-types -export function filter(pred, xs) { - if (is_null(xs)) { - return xs; - } else { - if (pred(head(xs))) { - return pair(head(xs), filter(pred, tail(xs))); - } else { - return filter(pred, tail(xs)); - } - } -} - -// enumerates numbers starting from start, -// using a step size of 1, until the number -// exceeds end. -export function enum_list(start, end) { - if (typeof start !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - start - ); - } - if (typeof end !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - end - ); - } - if (start > end) { - return null; - } else { - return pair(start, enum_list(start + 1, end)); - } -} - -// Returns the item in list lst at index n (the first item is at position 0) -export function list_ref(xs, n) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'list_ref(xs, n) expects a positive integer as argument n, but encountered ' + - n - ); - } - for (; n > 0; --n) { - xs = tail(xs); - } - return head(xs); -} - -// accumulate applies given operation op to elements of a list -// in a right-to-left order, first apply op to the last element -// and an initial element, resulting in r1, then to the -// second-last element and r1, resulting in r2, etc, and finally -// to the first element and r_n-1, where n is the length of the -// list. -// accumulate(op,zero,list(1,2,3)) results in -// op(1, op(2, op(3, zero))) -export function accumulate(op, initial, sequence) { - if (is_null(sequence)) { - return initial; - } else { - return op(head(sequence), accumulate(op, initial, tail(sequence))); - } -} - -// set_head(xs,x) changes the head of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_head(xs, x) { - if (is_pair(xs)) { - xs[0] = x; - return undefined; - } else { - throw new Error( - 'set_head(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// set_tail(xs,x) changes the tail of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_tail(xs, x) { - if (is_pair(xs)) { - xs[1] = x; - return undefined; - } else { - throw new Error( - 'set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} diff --git a/src/bundles/testing/__tests__/index.ts b/src/bundles/unittest/__tests__/index.ts similarity index 76% rename from src/bundles/testing/__tests__/index.ts rename to src/bundles/unittest/__tests__/index.ts index b1da9e1345..46a1b5d555 100644 --- a/src/bundles/testing/__tests__/index.ts +++ b/src/bundles/unittest/__tests__/index.ts @@ -1,16 +1,16 @@ import * as asserts from '../asserts'; import * as testing from '../functions'; -import { list } from '../list'; +import { list } from 'js-slang/dist/stdlib/list'; beforeAll(() => { - testing.context.suiteResults = { + testing.testContext.suiteResults = { name: '', results: [], total: 0, passed: 0, }; - testing.context.allResults.results = []; - testing.context.runtime = 0; + testing.testContext.allResults.results = []; + testing.testContext.runtime = 0; }); test('context is created correctly', () => { @@ -18,7 +18,7 @@ test('context is created correctly', () => { testing.describe('Testing 321', () => { testing.it('Testing 123', mockTestFn); }); - expect(testing.context.suiteResults.passed).toEqual(1); + expect(testing.testContext.suiteResults.passed).toEqual(1); expect(mockTestFn).toHaveBeenCalled(); }); @@ -26,8 +26,8 @@ test('context fails correctly', () => { testing.describe('Testing 123', () => { testing.it('This test fails!', () => asserts.assert_equals(0, 1)); }); - expect(testing.context.suiteResults.passed).toEqual(0); - expect(testing.context.suiteResults.total).toEqual(1); + expect(testing.testContext.suiteResults.passed).toEqual(0); + expect(testing.testContext.suiteResults.total).toEqual(1); }); test('assert works', () => { diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/unittest/asserts.ts new file mode 100644 index 0000000000..fc0b3b967e --- /dev/null +++ b/src/bundles/unittest/asserts.ts @@ -0,0 +1,105 @@ +import { is_pair, head, tail, is_list, is_null, length, type List, type Pair } from 'js-slang/dist/stdlib/list'; + +/** + * Asserts that a predicate returns true. + * @param pred An predicate function that returns true/false. + * @returns + */ +export function assert(pred: () => boolean) { + if (!pred()) { + throw new Error('Assert failed!'); + } +} + +/** + * Asserts the equality (===) of two parameters. + * @param expected The expected value. + * @param received The given value. + * @returns + */ +export function assert_equals(expected: any, received: any) { + const fail = () => { + throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); + }; + if (typeof expected !== typeof received) { + fail(); + } + // approx checking for floats + if (typeof expected === 'number' && !Number.isInteger(expected)) { + if (Math.abs(expected - received) > 0.001) { + fail(); + } else { + return; + } + } + if (expected !== received) { + fail(); + } +} + +/** + * Asserts that `xs` contains `toContain`. + * @param xs The list to assert. + * @param toContain The element that `xs` is expected to contain. + */ +export function assert_contains(xs: any, toContain: any) { + const fail = () => { + throw new Error(`Expected \`${xs}\` to contain \`${toContain}\`.`); + }; + + const member = (xs: List | Pair, item: any) => { + if (is_null(xs)) return false; + + if (is_list(xs)) { + if (head(xs) === item) return true; + return member(tail(xs), item); + } + + if (is_pair(xs)) { + return member(head(xs), item) || member(tail(xs), item); + } + + throw new Error(`First argument to ${assert_contains.name} must be a list or a pair, got \`${xs}\`.`); + }; + if (!member(xs, toContain)) fail(); +} + +/** + * Asserts that the given list has length `len`. + * @param list The list to assert. + * @param len The expected length of the list. + */ +export function assert_length(list: any, len: number) { + assert_equals(length(list), len); +} + +/** + * Asserts that the given item is greater than `expected` + * @param item The number to check + * @param expected The value to check against + */ + +export function assert_greater(item: any, expected: number) { + if (typeof item !== 'number') { + throw new Error(`${assert_greater.name} should be called with a numeric argument!`) + } + + if (item <= expected) { + throw new Error(`Expected ${item} to be greater than ${expected}`) + } +} + +/** + * Asserts that the given item is greater than or equal to `expected` + * @param item The number to check + * @param expected The value to check against + */ +export function assert_greater_equals(item: any, expected: number) { + if (typeof item !== 'number') { + throw new Error(`${assert_greater.name} should be called with a numeric argument!`) + } + + if (item < expected) { + throw new Error(`Expected ${item} to be greater than or equal to ${expected}`) + } +} \ No newline at end of file diff --git a/src/bundles/testing/functions.ts b/src/bundles/unittest/functions.ts similarity index 57% rename from src/bundles/testing/functions.ts rename to src/bundles/unittest/functions.ts index d844ef234f..057486d8d1 100644 --- a/src/bundles/testing/functions.ts +++ b/src/bundles/unittest/functions.ts @@ -1,3 +1,5 @@ +import context from 'js-slang/context'; + import type { TestContext, TestSuite, Test } from './types'; const handleErr = (err: any) => { @@ -10,10 +12,17 @@ const handleErr = (err: any) => { throw err; }; -export const context: TestContext = { - describe: (msg: string, suite: TestSuite) => { +export const testContext: TestContext = { + called: false, + describe(msg: string, suite: TestSuite) { + if (this.called) { + throw new Error(`${describe.name} can only be called once per program!`) + } + + this.called = true; + const starttime = performance.now(); - context.suiteResults = { + testContext.suiteResults = { name: msg, results: [], total: 0, @@ -22,26 +31,26 @@ export const context: TestContext = { suite(); - context.allResults.results.push(context.suiteResults); + testContext.allResults.results.push(testContext.suiteResults); const endtime = performance.now(); - context.runtime += endtime - starttime; - return context.allResults; + testContext.runtime += endtime - starttime; + return testContext.allResults; }, - it: (msg: string, test: Test) => { + it(msg: string, test: Test) { const name = `${msg}`; let error = ''; - context.suiteResults.total += 1; + this.suiteResults.total += 1; try { test(); - context.suiteResults.passed += 1; + this.suiteResults.passed += 1; } catch (err: any) { error = handleErr(err); } - context.suiteResults.results.push({ + this.suiteResults.results.push({ name, error, }); @@ -57,19 +66,21 @@ export const context: TestContext = { allResults: { results: [], toReplString: () => - `${context.allResults.results.length} suites completed in ${context.runtime} ms.`, + `${testContext.allResults.results.length} suites completed in ${testContext.runtime} ms.`, }, runtime: 0, }; +context.moduleContexts.unittest.state = testContext + /** * Defines a single test. * @param str Description for this test. * @param func Function containing tests. */ export function it(msg: string, func: Test) { - context.it(msg, func); + testContext.it(msg, func); } /** @@ -78,5 +89,5 @@ export function it(msg: string, func: Test) { * @param func Function containing tests. */ export function describe(msg: string, func: TestSuite) { - return context.describe(msg, func); + return testContext.describe(msg, func); } diff --git a/src/bundles/testing/index.ts b/src/bundles/unittest/index.ts similarity index 70% rename from src/bundles/testing/index.ts rename to src/bundles/unittest/index.ts index 83adf922c4..2caba3836a 100644 --- a/src/bundles/testing/index.ts +++ b/src/bundles/unittest/index.ts @@ -1,35 +1,34 @@ -import { - assert_equals, - assert_not_equals, - assert_contains, - assert_length, -} from './asserts'; -import { it, describe } from './functions'; -import { mock_fn } from './mocks'; - -/** - * Collection of unit-testing tools for Source. - * @author Jia Xiaodong - */ - -/** - * Increment a number by a value of 1. - * @param x the number to be incremented - * @returns the incremented value of the number - */ -function sample_function(x: number) { - return x + 1; -} - -// Un-comment the next line if your bundle requires the use of variables -// declared in cadet-frontend or js-slang. -export default () => ({ - sample_function, - it, - describe, - assert_equals, - assert_not_equals, - assert_contains, - assert_length, - mock_fn, -}); +/** + * Collection of unit-testing tools for Source. + * @author Jia Xiaodong + */ + +import { + assert_equals, + assert_contains, + assert_greater, + assert_greater_equals, + assert_length, +} from './asserts'; +import { it, describe } from './functions'; +import { mock_fn } from './mocks'; +/** + * Increment a number by a value of 1. + * @param x the number to be incremented + * @returns the incremented value of the number + */ +function sample_function(x: number) { + return x + 1; +} + +export default { + sample_function, + it, + describe, + assert_equals, + assert_contains, + assert_greater, + assert_greater_equals, + assert_length, + mock_fn, +}; diff --git a/src/bundles/testing/mocks.ts b/src/bundles/unittest/mocks.ts similarity index 91% rename from src/bundles/testing/mocks.ts rename to src/bundles/unittest/mocks.ts index 139f06aa9c..92019dcc7a 100644 --- a/src/bundles/testing/mocks.ts +++ b/src/bundles/unittest/mocks.ts @@ -1,4 +1,4 @@ -import { pair, list, vector_to_list } from './list'; +import { pair, list, vector_to_list } from 'js-slang/dist/stdlib/list'; /* eslint-disable import/prefer-default-export */ /** diff --git a/src/bundles/testing/types.ts b/src/bundles/unittest/types.ts similarity index 97% rename from src/bundles/testing/types.ts rename to src/bundles/unittest/types.ts index 0d8102ccfc..4fd7859681 100644 --- a/src/bundles/testing/types.ts +++ b/src/bundles/unittest/types.ts @@ -5,6 +5,7 @@ export type ErrorLogger = ( export type Test = () => void; export type TestSuite = () => void; export type TestContext = { + called: boolean; describe: (msg: string, tests: TestSuite) => Results; it: (msg: string, test: Test) => void; // This holds the result of a single suite and is cleared on every run diff --git a/src/tabs/Testing/index.tsx b/src/tabs/Testing/index.tsx deleted file mode 100644 index 2c9d70ce32..0000000000 --- a/src/tabs/Testing/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import type { Results, SuiteResult } from '../../bundles/testing/types'; - -/** - * Tab for unit tests. - * @author Jia Xiaodong - */ - -type Props = { - result: any; -}; - -class TestSuitesTab extends React.PureComponent { - /** - * Converts the results of a test suite run into a table format in its own div. - */ - private static suiteResultToDiv(suiteResult: SuiteResult) { - const { name, results, total, passed } = suiteResult; - const colfixed = { - border: '1px solid gray', - overflow: 'hidden', - width: 200, - }; - const colauto = { - border: '1px solid gray', - overflow: 'hidden', - width: 'auto', - }; - - const rows = results.map(({ name: testname, error }, index) => ( - // eslint-disable-next-line react/no-array-index-key - - {testname} - {error || 'Passed.'} - - )); - - const tablestyle = { - 'table-layout': 'fixed', - width: '100%', - }; - const table = ( - - - - - - - - {rows} -
Test caseMessages
- ); - - const suitestyle = { - border: '1px solid white', - padding: 5, - margin: 5, - }; - return ( -
-

- {name} -

-

- Passed testcases: {passed}/{total} -

- {table} -
- ); - } - - public render() { - const { result: res } = this.props; - const block = res.results.map((suiteresult: SuiteResult) => - TestSuitesTab.suiteResultToDiv(suiteresult) - ); - - return ( -
-

The following is a report of your tests.

- {block} -
- ); - } -} - -export default { - /** - * This function will be called to determine if the component will be - * rendered. - * @param {DebuggerContext} context - * @returns {boolean} - */ - toSpawn: (context: any): boolean => { - function valid(value: any): value is Results { - try { - return ( - value instanceof Object && - Array.isArray(value.results) && - Array.isArray(value.results[0].results) - ); - } catch (e) { - return false; - } - } - return valid(context.result.value); - }, - - /** - * This function will be called to render the module tab in the side contents - * on Source Academy frontend. - * @param {DebuggerContext} context - */ - // eslint-disable-next-line react/destructuring-assignment - body: (context: any) => , - - /** - * The Tab's icon tooltip in the side contents on Source Academy frontend. - */ - label: 'Test suites', - - /** - * BlueprintJS IconName element's name, used to render the icon which will be - * displayed in the side contents panel. - * @see https://blueprintjs.com/docs/#icons - */ - iconName: 'lab-test', -}; diff --git a/src/tabs/Unittest/index.tsx b/src/tabs/Unittest/index.tsx new file mode 100644 index 0000000000..4908c96575 --- /dev/null +++ b/src/tabs/Unittest/index.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import type { SuiteResult, TestContext } from '../../bundles/unittest/types'; +import { getModuleState, type DebuggerContext } from '../../typings/type_helpers'; + +/** + * Tab for unit tests. + * @author Jia Xiaodong + */ + +type Props = { + context: TestContext; +} + +/** + * Converts the results of a test suite run into a table format in its own div. + */ +function suiteResultToDiv(suiteResult: SuiteResult) { + const { name, results, total, passed } = suiteResult; + if (results.length === 0) { + return
+ Your test suite did not contain any tests! +
+ } + + const colfixed = { + border: '1px solid gray', + overflow: 'hidden', + width: 200, + }; + const colauto = { + border: '1px solid gray', + overflow: 'hidden', + width: 'auto', + }; + + const rows = results.map(({ name: testname, error }, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {testname} + {error || 'Passed.'} + + )); + + const tablestyle = { + 'table-layout': 'fixed', + width: '100%', + }; + const table = ( + + + + + + + + {rows} +
Test caseMessages
+ ); + + const suitestyle = { + border: '1px solid white', + padding: 5, + margin: 5, + }; + return ( +
+

+ {name} +

+

+ Passed testcases: {passed}/{total} +

+ {table} +
+ ); +} + +class TestSuitesTab extends React.PureComponent { + public render() { + const { context: { suiteResults, called } } = this.props; + + if (!called) { + return
+ Call describe at least once to be able to view the results of your tests +
+ } + + const block = suiteResultToDiv(suiteResults) + + return ( +
+

The following is a report of your tests.

+ {block} +
+ ); + } +} + +export default { + toSpawn: (context: DebuggerContext): boolean => true, + + /** + * This function will be called to render the module tab in the side contents + * on Source Academy frontend. + * @param {DebuggerContext} context + */ + // eslint-disable-next-line react/destructuring-assignment + body: (context: DebuggerContext) => { + const moduleContext = getModuleState(context, 'unittest') + return ; + }, + + /** + * The Tab's icon tooltip in the side contents on Source Academy frontend. + */ + label: 'Test suites', + + /** + * BlueprintJS IconName element's name, used to render the icon which will be + * displayed in the side contents panel. + * @see https://blueprintjs.com/docs/#icons + */ + iconName: 'lab-test', +}; From 08f285e39bdbe22f8660344616be4b3a533ae508 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Wed, 26 Feb 2025 02:06:44 -0500 Subject: [PATCH 2/6] Update test --- src/bundles/unittest/__tests__/index.ts | 3 ++- src/bundles/unittest/functions.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/bundles/unittest/__tests__/index.ts b/src/bundles/unittest/__tests__/index.ts index 46a1b5d555..1af0a3dfdd 100644 --- a/src/bundles/unittest/__tests__/index.ts +++ b/src/bundles/unittest/__tests__/index.ts @@ -2,7 +2,7 @@ import * as asserts from '../asserts'; import * as testing from '../functions'; import { list } from 'js-slang/dist/stdlib/list'; -beforeAll(() => { +beforeEach(() => { testing.testContext.suiteResults = { name: '', results: [], @@ -11,6 +11,7 @@ beforeAll(() => { }; testing.testContext.allResults.results = []; testing.testContext.runtime = 0; + testing.testContext.called = false; }); test('context is created correctly', () => { diff --git a/src/bundles/unittest/functions.ts b/src/bundles/unittest/functions.ts index 057486d8d1..f3965cd183 100644 --- a/src/bundles/unittest/functions.ts +++ b/src/bundles/unittest/functions.ts @@ -22,7 +22,7 @@ export const testContext: TestContext = { this.called = true; const starttime = performance.now(); - testContext.suiteResults = { + this.suiteResults = { name: msg, results: [], total: 0, @@ -31,11 +31,11 @@ export const testContext: TestContext = { suite(); - testContext.allResults.results.push(testContext.suiteResults); + this.allResults.results.push(this.suiteResults); const endtime = performance.now(); - testContext.runtime += endtime - starttime; - return testContext.allResults; + this.runtime += endtime - starttime; + return this.allResults; }, it(msg: string, test: Test) { From cb4a5252f79430bce0bc0e267fb1f1e6395bba63 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Wed, 26 Feb 2025 02:13:51 -0500 Subject: [PATCH 3/6] Fix linting --- src/bundles/unittest/__tests__/index.ts | 2 +- src/bundles/unittest/asserts.ts | 10 +++++----- src/bundles/unittest/functions.ts | 4 ++-- src/tabs/Unittest/index.tsx | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/bundles/unittest/__tests__/index.ts b/src/bundles/unittest/__tests__/index.ts index 1af0a3dfdd..1d8c2a380d 100644 --- a/src/bundles/unittest/__tests__/index.ts +++ b/src/bundles/unittest/__tests__/index.ts @@ -1,6 +1,6 @@ +import { list } from 'js-slang/dist/stdlib/list'; import * as asserts from '../asserts'; import * as testing from '../functions'; -import { list } from 'js-slang/dist/stdlib/list'; beforeEach(() => { testing.testContext.suiteResults = { diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/unittest/asserts.ts index fc0b3b967e..3eb3b023aa 100644 --- a/src/bundles/unittest/asserts.ts +++ b/src/bundles/unittest/asserts.ts @@ -81,11 +81,11 @@ export function assert_length(list: any, len: number) { export function assert_greater(item: any, expected: number) { if (typeof item !== 'number') { - throw new Error(`${assert_greater.name} should be called with a numeric argument!`) + throw new Error(`${assert_greater.name} should be called with a numeric argument!`); } if (item <= expected) { - throw new Error(`Expected ${item} to be greater than ${expected}`) + throw new Error(`Expected ${item} to be greater than ${expected}`); } } @@ -96,10 +96,10 @@ export function assert_greater(item: any, expected: number) { */ export function assert_greater_equals(item: any, expected: number) { if (typeof item !== 'number') { - throw new Error(`${assert_greater.name} should be called with a numeric argument!`) + throw new Error(`${assert_greater.name} should be called with a numeric argument!`); } if (item < expected) { - throw new Error(`Expected ${item} to be greater than or equal to ${expected}`) + throw new Error(`Expected ${item} to be greater than or equal to ${expected}`); } -} \ No newline at end of file +} diff --git a/src/bundles/unittest/functions.ts b/src/bundles/unittest/functions.ts index f3965cd183..c068ef2f24 100644 --- a/src/bundles/unittest/functions.ts +++ b/src/bundles/unittest/functions.ts @@ -16,7 +16,7 @@ export const testContext: TestContext = { called: false, describe(msg: string, suite: TestSuite) { if (this.called) { - throw new Error(`${describe.name} can only be called once per program!`) + throw new Error(`${describe.name} can only be called once per program!`); } this.called = true; @@ -72,7 +72,7 @@ export const testContext: TestContext = { runtime: 0, }; -context.moduleContexts.unittest.state = testContext +context.moduleContexts.unittest.state = testContext; /** * Defines a single test. diff --git a/src/tabs/Unittest/index.tsx b/src/tabs/Unittest/index.tsx index 4908c96575..18a669025f 100644 --- a/src/tabs/Unittest/index.tsx +++ b/src/tabs/Unittest/index.tsx @@ -9,7 +9,7 @@ import { getModuleState, type DebuggerContext } from '../../typings/type_helpers type Props = { context: TestContext; -} +}; /** * Converts the results of a test suite run into a table format in its own div. @@ -19,7 +19,7 @@ function suiteResultToDiv(suiteResult: SuiteResult) { if (results.length === 0) { return
Your test suite did not contain any tests! -
+ ; } const colfixed = { @@ -82,10 +82,10 @@ class TestSuitesTab extends React.PureComponent { if (!called) { return
Call describe at least once to be able to view the results of your tests -
+ ; } - const block = suiteResultToDiv(suiteResults) + const block = suiteResultToDiv(suiteResults); return (
@@ -106,7 +106,7 @@ export default { */ // eslint-disable-next-line react/destructuring-assignment body: (context: DebuggerContext) => { - const moduleContext = getModuleState(context, 'unittest') + const moduleContext = getModuleState(context, 'unittest'); return ; }, From bc9447a7c1b9e96074ee90ad5f8c7af033509f16 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:05:12 +0800 Subject: [PATCH 4/6] Restore docs and remove lint warning --- src/tabs/Unittest/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tabs/Unittest/index.tsx b/src/tabs/Unittest/index.tsx index 18a669025f..a84ecb0f48 100644 --- a/src/tabs/Unittest/index.tsx +++ b/src/tabs/Unittest/index.tsx @@ -97,7 +97,13 @@ class TestSuitesTab extends React.PureComponent { } export default { - toSpawn: (context: DebuggerContext): boolean => true, + /** + * This function will be called to determine if the component will be + * rendered. + * @param {DebuggerContext} context + * @returns {boolean} + */ + toSpawn: (_context: DebuggerContext): boolean => true, /** * This function will be called to render the module tab in the side contents From b98758204c90821920a43fe58ffa0019247f6a7e Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:12:26 +0800 Subject: [PATCH 5/6] Restore `assert_not_equals` --- src/bundles/unittest/asserts.ts | 12 ++++++++++++ src/bundles/unittest/index.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/unittest/asserts.ts index 3eb3b023aa..8f0a24de1a 100644 --- a/src/bundles/unittest/asserts.ts +++ b/src/bundles/unittest/asserts.ts @@ -37,6 +37,18 @@ export function assert_equals(expected: any, received: any) { } } +/** + * Asserts that two parameters are not equal (!==). + * @param expected The expected value. + * @param received The given value. + * @returns + */ +export function assert_not_equals(expected: any, received: any) { + if (expected === received) { + throw new Error(`Expected \`${expected}\` to not equal \`${received}\`!`); + } +} + /** * Asserts that `xs` contains `toContain`. * @param xs The list to assert. diff --git a/src/bundles/unittest/index.ts b/src/bundles/unittest/index.ts index 2caba3836a..d7c9d79e79 100644 --- a/src/bundles/unittest/index.ts +++ b/src/bundles/unittest/index.ts @@ -5,6 +5,7 @@ import { assert_equals, + assert_not_equals, assert_contains, assert_greater, assert_greater_equals, @@ -26,6 +27,7 @@ export default { it, describe, assert_equals, + assert_not_equals, assert_contains, assert_greater, assert_greater_equals, From 2bc12365f71344aa81d635f1d8de6c7adc815d1d Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:13:24 +0800 Subject: [PATCH 6/6] Strengthen typecheck --- src/bundles/unittest/asserts.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/unittest/asserts.ts index 8f0a24de1a..154c794195 100644 --- a/src/bundles/unittest/asserts.ts +++ b/src/bundles/unittest/asserts.ts @@ -90,10 +90,9 @@ export function assert_length(list: any, len: number) { * @param item The number to check * @param expected The value to check against */ - export function assert_greater(item: any, expected: number) { - if (typeof item !== 'number') { - throw new Error(`${assert_greater.name} should be called with a numeric argument!`); + if (typeof item !== 'number' || typeof expected !== 'number') { + throw new Error(`${assert_greater.name} should be called with numeric arguments!`); } if (item <= expected) { @@ -107,8 +106,8 @@ export function assert_greater(item: any, expected: number) { * @param expected The value to check against */ export function assert_greater_equals(item: any, expected: number) { - if (typeof item !== 'number') { - throw new Error(`${assert_greater.name} should be called with a numeric argument!`); + if (typeof item !== 'number' || typeof expected !== 'number') { + throw new Error(`${assert_greater.name} should be called with numeric arguments!`); } if (item < expected) {