From d8e8c798e9541aecac00b4ca2da03bee50691499 Mon Sep 17 00:00:00 2001 From: J XD Date: Tue, 14 Dec 2021 15:16:27 +0800 Subject: [PATCH 1/6] initial commit --- modules.json | 7 +- src/bundles/unittest/asserts.ts | 128 ++++++++++ src/bundles/unittest/functions.ts | 82 +++++++ src/bundles/unittest/index.ts | 41 ++++ src/bundles/unittest/list.ts | 372 ++++++++++++++++++++++++++++++ src/bundles/unittest/mocks.ts | 32 +++ src/bundles/unittest/types.ts | 29 +++ src/tabs/UnitTest/index.tsx | 128 ++++++++++ 8 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 src/bundles/unittest/asserts.ts create mode 100644 src/bundles/unittest/functions.ts create mode 100644 src/bundles/unittest/index.ts create mode 100644 src/bundles/unittest/list.ts create mode 100644 src/bundles/unittest/mocks.ts create mode 100644 src/bundles/unittest/types.ts create mode 100644 src/tabs/UnitTest/index.tsx diff --git a/modules.json b/modules.json index c978ca499..1d8af8b0f 100644 --- a/modules.json +++ b/modules.json @@ -59,5 +59,10 @@ "tabs": [ "SoundMatrix" ] + }, + "unittest": { + "tabs": [ + "UnitTest" + ] } -} +} \ No newline at end of file diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/unittest/asserts.ts new file mode 100644 index 000000000..204f023c3 --- /dev/null +++ b/src/bundles/unittest/asserts.ts @@ -0,0 +1,128 @@ +import { is_pair, head, tail, is_list, is_null, member, length } from './list'; + +/** + * Asserts the equality (===) of the two parameters. + * @param expected The expected value. + * @param received The given value. + * @returns + */ +export function assert_equals(expected: any, received: any) { + if (expected !== received) { + throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); + } +} + +/** + * Asserts the inequality (!==) of the two parameters. + * @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 not equal \`${expected}\`!`); + } +} + +/** + * Asserts the inequality (!==) of the two parameters. + * @param expected The expected value. + * @param received The given value. + * @returns + */ +export function assert_approx_equals(expected: number, received: number) { + if (Math.abs(expected - received) > 0.001) { + throw new Error(`Expected \`${expected}\` to approx. \`${received}\`!`); + } +} + +/** + * Asserts that `expected` > `received`. + * @param expected + * @param received + */ +export function assert_greater(expected: number, received: number) { + if (expected <= received) { + throw new Error(`Expected \`${expected}\` > \`${received}\`!`); + } +} + +/** + * Asserts that `expected` >= `received`. + * @param expected + * @param received + */ +export function assert_greater_equals(expected: number, received: number) { + if (expected < received) { + throw new Error(`Expected \`${expected}\` >= \`${received}\`!`); + } +} + +/** + * Asserts that `expected` < `received`. + * @param expected + * @param received + */ +export function assert_lesser(expected: number, received: number) { + if (expected >= received) { + throw new Error(`Expected \`${expected}\` < \`${received}\`!`); + } +} + +/** + * Asserts that `expected` <= `received`. + * @param expected + * @param received + */ +export function assert_lesser_equals(expected: number, received: number) { + if (expected > received) { + throw new Error(`Expected \`${expected}\` <= \`${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/unittest/functions.ts b/src/bundles/unittest/functions.ts new file mode 100644 index 000000000..38ba819fb --- /dev/null +++ b/src/bundles/unittest/functions.ts @@ -0,0 +1,82 @@ +import { TestContext, TestSuite, Test } from './types'; + +const handleErr = (err: any) => { + if (err.error && err.error.message) { + return (err.error as Error).message; + } + if (err.message) { + return (err as Error).message; + } + throw err; +}; + +export const context: TestContext = { + describe: (msg: string, suite: TestSuite) => { + const starttime = performance.now(); + context.suiteResults = { + name: msg, + results: [], + total: 0, + passed: 0, + }; + + suite(); + + context.allResults.results.push(context.suiteResults); + + const endtime = performance.now(); + context.runtime += endtime - starttime; + return context.allResults; + }, + + it: (msg: string, test: Test) => { + const name = `${msg}`; + let error = ''; + context.suiteResults.total += 1; + + try { + test(); + } catch (err: any) { + error = handleErr(err); + } + + context.suiteResults.passed += 1; + context.suiteResults.results.push({ + name, + error, + }); + }, + + suiteResults: { + name: '', + results: [], + total: 0, + passed: 0, + }, + + allResults: { + results: [], + toReplString: () => + `${context.allResults.results.length} suites completed in ${context.runtime} ms.`, + }, + + runtime: 0, +}; + +/** + * 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); +} + +/** + * Describes a test suite. + * @param str Description for this test. + * @param func Function containing tests. + */ +export function describe(msg: string, func: TestSuite) { + return context.describe(msg, func); +} diff --git a/src/bundles/unittest/index.ts b/src/bundles/unittest/index.ts new file mode 100644 index 000000000..ae85ec80d --- /dev/null +++ b/src/bundles/unittest/index.ts @@ -0,0 +1,41 @@ +import { it, describe } from './functions'; +import { + assert_equals, + assert_not_equals, + assert_contains, + assert_approx_equals, + assert_greater, + assert_greater_equals, + assert_length, +} from './asserts'; +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_greater, + assert_greater_equals, + assert_approx_equals, + assert_length, + mock_fn, +}); diff --git a/src/bundles/unittest/list.ts b/src/bundles/unittest/list.ts new file mode 100644 index 000000000..e5205937c --- /dev/null +++ b/src/bundles/unittest/list.ts @@ -0,0 +1,372 @@ +/* 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/unittest/mocks.ts b/src/bundles/unittest/mocks.ts new file mode 100644 index 000000000..139f06aa9 --- /dev/null +++ b/src/bundles/unittest/mocks.ts @@ -0,0 +1,32 @@ +import { pair, list, vector_to_list } from './list'; +/* eslint-disable import/prefer-default-export */ + +/** + * Mocks a function `fun`. + * @param fun The function to mock. + * @returns A pair whose head is the mocked function, and whose tail is another + * function that returns a list with details about the mocked function. + */ +export function mock_fn(fun: any) { + function details(count, retvals, arglist) { + return list( + 'times called', + count, + 'Return values', + retvals, + 'Arguments', + arglist + ); + } + let count = 0; + let retvals: any = null; + let arglist: any = null; + function fn(...args) { + count += 1; + const retval = fun.apply(fun, args); + retvals = pair(retval, retvals); + arglist = pair(vector_to_list(args), arglist); + return retval; + } + return pair(fn, () => details(count, retvals, arglist)); +} diff --git a/src/bundles/unittest/types.ts b/src/bundles/unittest/types.ts new file mode 100644 index 000000000..0f9558c37 --- /dev/null +++ b/src/bundles/unittest/types.ts @@ -0,0 +1,29 @@ +export type ErrorLogger = ( + error: string | string[], + isSlangError?: boolean +) => void; +export type Test = () => void; +export type TestSuite = () => void; +export type TestContext = { + 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 + suiteResults: SuiteResult; + // This holds the results of the entire suite + allResults: Results; + runtime: number; +}; +export type TestResult = { + name: string; + error: string; +}; +export type SuiteResult = { + name: string; + results: TestResult[]; + total: number; + passed: number; +}; +export type Results = { + results: SuiteResult[]; + toReplString: () => string; +}; diff --git a/src/tabs/UnitTest/index.tsx b/src/tabs/UnitTest/index.tsx new file mode 100644 index 000000000..87890d62d --- /dev/null +++ b/src/tabs/UnitTest/index.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Results, SuiteResult } from '../../bundles/unittest/types'; + +/** + * Tab for unit tests. + * @author Jia Xiaodong + */ + +type Props = { + result: any; +}; + +class UnitTests extends React.PureComponent { + /** + * Converts the results of a test suite run into a table format in its own div. + */ + private static suiteResultToDiv(suiteResult: any) { + const { name, results, total, passed } = suiteResult as 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) => + UnitTests.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: 'Unit Tests', + + /** + * 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 d9b1127bad0b977b7061da8c440921205f526fd7 Mon Sep 17 00:00:00 2001 From: J XD Date: Mon, 28 Feb 2022 16:36:01 +0800 Subject: [PATCH 2/6] fixes and add test --- modules.json | 4 +-- src/bundles/testing/__tests__/index.ts | 34 +++++++++++++++++++ src/bundles/{unittest => testing}/asserts.ts | 0 .../{unittest => testing}/functions.ts | 2 +- src/bundles/{unittest => testing}/index.ts | 0 src/bundles/{unittest => testing}/list.ts | 0 src/bundles/{unittest => testing}/mocks.ts | 0 src/bundles/{unittest => testing}/types.ts | 0 src/tabs/{UnitTest => Testing}/index.tsx | 14 ++++---- 9 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 src/bundles/testing/__tests__/index.ts rename src/bundles/{unittest => testing}/asserts.ts (100%) rename src/bundles/{unittest => testing}/functions.ts (97%) rename src/bundles/{unittest => testing}/index.ts (100%) rename src/bundles/{unittest => testing}/list.ts (100%) rename src/bundles/{unittest => testing}/mocks.ts (100%) rename src/bundles/{unittest => testing}/types.ts (100%) rename src/tabs/{UnitTest => Testing}/index.tsx (83%) diff --git a/modules.json b/modules.json index 89479304d..5496063db 100644 --- a/modules.json +++ b/modules.json @@ -55,9 +55,9 @@ "SoundMatrix" ] }, - "unittest": { + "testing": { "tabs": [ - "UnitTest" + "Testing" ] } } \ No newline at end of file diff --git a/src/bundles/testing/__tests__/index.ts b/src/bundles/testing/__tests__/index.ts new file mode 100644 index 000000000..01dc9e794 --- /dev/null +++ b/src/bundles/testing/__tests__/index.ts @@ -0,0 +1,34 @@ +import * as testing from '../functions'; +import * as asserts from '../asserts'; + +beforeAll(() => { + testing.context.suiteResults = { + name: '', + results: [], + total: 0, + passed: 0, + }; + testing.context.allResults.results = []; + testing.context.runtime = 0; +}); + +test('Test context is created correctly', () => { + const mockTestFn = jest.fn(); + testing.describe('Testing 321', () => { + testing.it('Testing 123', mockTestFn); + }); + expect(testing.context.suiteResults.passed).toEqual(1); + expect(mockTestFn).toHaveBeenCalled(); +}); + +test('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); +}); + +test('Assert equal works', () => { + expect(() => asserts.assert_equals(0, 1)).toThrow('Expected'); +}); diff --git a/src/bundles/unittest/asserts.ts b/src/bundles/testing/asserts.ts similarity index 100% rename from src/bundles/unittest/asserts.ts rename to src/bundles/testing/asserts.ts diff --git a/src/bundles/unittest/functions.ts b/src/bundles/testing/functions.ts similarity index 97% rename from src/bundles/unittest/functions.ts rename to src/bundles/testing/functions.ts index 38ba819fb..bb28f108f 100644 --- a/src/bundles/unittest/functions.ts +++ b/src/bundles/testing/functions.ts @@ -36,11 +36,11 @@ export const context: TestContext = { try { test(); + context.suiteResults.passed += 1; } catch (err: any) { error = handleErr(err); } - context.suiteResults.passed += 1; context.suiteResults.results.push({ name, error, diff --git a/src/bundles/unittest/index.ts b/src/bundles/testing/index.ts similarity index 100% rename from src/bundles/unittest/index.ts rename to src/bundles/testing/index.ts diff --git a/src/bundles/unittest/list.ts b/src/bundles/testing/list.ts similarity index 100% rename from src/bundles/unittest/list.ts rename to src/bundles/testing/list.ts diff --git a/src/bundles/unittest/mocks.ts b/src/bundles/testing/mocks.ts similarity index 100% rename from src/bundles/unittest/mocks.ts rename to src/bundles/testing/mocks.ts diff --git a/src/bundles/unittest/types.ts b/src/bundles/testing/types.ts similarity index 100% rename from src/bundles/unittest/types.ts rename to src/bundles/testing/types.ts diff --git a/src/tabs/UnitTest/index.tsx b/src/tabs/Testing/index.tsx similarity index 83% rename from src/tabs/UnitTest/index.tsx rename to src/tabs/Testing/index.tsx index 87890d62d..62df40cc5 100644 --- a/src/tabs/UnitTest/index.tsx +++ b/src/tabs/Testing/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Results, SuiteResult } from '../../bundles/unittest/types'; +import { Results, SuiteResult } from '../../bundles/testing/types'; /** * Tab for unit tests. @@ -10,12 +10,12 @@ type Props = { result: any; }; -class UnitTests extends React.PureComponent { +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: any) { - const { name, results, total, passed } = suiteResult as SuiteResult; + private static suiteResultToDiv(suiteResult: SuiteResult) { + const { name, results, total, passed } = suiteResult; const colfixed = { border: '1px solid gray', overflow: 'hidden', @@ -72,7 +72,7 @@ class UnitTests extends React.PureComponent { public render() { const { result: res } = this.props; const block = res.results.map((suiteresult: SuiteResult) => - UnitTests.suiteResultToDiv(suiteresult) + TestSuitesTab.suiteResultToDiv(suiteresult) ); return ( @@ -112,12 +112,12 @@ export default { * @param {DebuggerContext} context */ // eslint-disable-next-line react/destructuring-assignment - body: (context: any) => , + body: (context: any) => , /** * The Tab's icon tooltip in the side contents on Source Academy frontend. */ - label: 'Unit Tests', + label: 'Test suites', /** * BlueprintJS IconName element's name, used to render the icon which will be From 3ce2b22b0cc9d5f4de6cef7f81ad2067de0e12b9 Mon Sep 17 00:00:00 2001 From: J XD Date: Mon, 28 Feb 2022 16:40:56 +0800 Subject: [PATCH 3/6] add more test --- src/bundles/testing/__tests__/index.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/bundles/testing/__tests__/index.ts b/src/bundles/testing/__tests__/index.ts index 01dc9e794..0d4891cff 100644 --- a/src/bundles/testing/__tests__/index.ts +++ b/src/bundles/testing/__tests__/index.ts @@ -1,5 +1,6 @@ import * as testing from '../functions'; import * as asserts from '../asserts'; +import { pair, list } from '../list'; beforeAll(() => { testing.context.suiteResults = { @@ -29,6 +30,23 @@ test('Test context fails correctly', () => { expect(testing.context.suiteResults.total).toEqual(1); }); -test('Assert equal works', () => { +test('assert_equals works', () => { + expect(() => asserts.assert_equals(1, 1)).not.toThrow(); expect(() => asserts.assert_equals(0, 1)).toThrow('Expected'); }); + +test('assert_not_equals works', () => { + expect(() => asserts.assert_not_equals(0, 1)).not.toThrow(); + expect(() => asserts.assert_not_equals(1, 1)).toThrow('Expected not'); +}); + +test('assert_greater works', () => { + expect(() => asserts.assert_equals(1, 1)).not.toThrow(); + expect(() => asserts.assert_equals(1, 0)).toThrow('Expected'); +}); + +test('assert_contains works', () => { + const list1 = list(1, 2, 3); + expect(() => asserts.assert_contains(list1, 2)).not.toThrow(); + expect(() => asserts.assert_contains(list1, 10)).toThrow(); +}); \ No newline at end of file From feed8a171a35874332d5b53dc55b7e2fd33bc63a Mon Sep 17 00:00:00 2001 From: J XD Date: Fri, 18 Mar 2022 11:43:18 +0800 Subject: [PATCH 4/6] remove redundant asserts and make a general one --- src/bundles/testing/__tests__/index.ts | 20 +++--- src/bundles/testing/asserts.ts | 85 +++++++------------------- 2 files changed, 29 insertions(+), 76 deletions(-) diff --git a/src/bundles/testing/__tests__/index.ts b/src/bundles/testing/__tests__/index.ts index 0d4891cff..6a025e2c8 100644 --- a/src/bundles/testing/__tests__/index.ts +++ b/src/bundles/testing/__tests__/index.ts @@ -1,6 +1,6 @@ import * as testing from '../functions'; import * as asserts from '../asserts'; -import { pair, list } from '../list'; +import { list } from '../list'; beforeAll(() => { testing.context.suiteResults = { @@ -30,23 +30,19 @@ test('Test context fails correctly', () => { expect(testing.context.suiteResults.total).toEqual(1); }); -test('assert_equals works', () => { - expect(() => asserts.assert_equals(1, 1)).not.toThrow(); - expect(() => asserts.assert_equals(0, 1)).toThrow('Expected'); -}); - -test('assert_not_equals works', () => { - expect(() => asserts.assert_not_equals(0, 1)).not.toThrow(); - expect(() => asserts.assert_not_equals(1, 1)).toThrow('Expected not'); +test('assert works', () => { + expect(() => asserts.assert(() => true)).not.toThrow(); + expect(() => asserts.assert(() => false)).toThrow('Assert failed'); }); -test('assert_greater works', () => { +test('assert_equals works', () => { expect(() => asserts.assert_equals(1, 1)).not.toThrow(); - expect(() => asserts.assert_equals(1, 0)).toThrow('Expected'); + expect(() => asserts.assert_equals(0, 1)).toThrow('Expected'); + expect(() => asserts.assert_equals(1.00000000001, 1)).not.toThrow(); }); test('assert_contains works', () => { const list1 = list(1, 2, 3); expect(() => asserts.assert_contains(list1, 2)).not.toThrow(); expect(() => asserts.assert_contains(list1, 10)).toThrow(); -}); \ No newline at end of file +}); diff --git a/src/bundles/testing/asserts.ts b/src/bundles/testing/asserts.ts index 204f023c3..bacf7920b 100644 --- a/src/bundles/testing/asserts.ts +++ b/src/bundles/testing/asserts.ts @@ -1,82 +1,39 @@ import { is_pair, head, tail, is_list, is_null, member, length } from './list'; /** - * Asserts the equality (===) of the two parameters. - * @param expected The expected value. - * @param received The given value. + * Asserts that a predicate returns true. + * @param pred An predicate function that returns true/false. * @returns */ -export function assert_equals(expected: any, received: any) { - if (expected !== received) { - throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); +export function assert(pred: () => boolean) { + if (!pred()) { + throw new Error(`Assert failed!`); } } /** - * Asserts the inequality (!==) of the two parameters. + * Asserts the equality (===) of two parameters. * @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 not equal \`${expected}\`!`); - } -} - -/** - * Asserts the inequality (!==) of the two parameters. - * @param expected The expected value. - * @param received The given value. - * @returns - */ -export function assert_approx_equals(expected: number, received: number) { - if (Math.abs(expected - received) > 0.001) { - throw new Error(`Expected \`${expected}\` to approx. \`${received}\`!`); - } -} - -/** - * Asserts that `expected` > `received`. - * @param expected - * @param received - */ -export function assert_greater(expected: number, received: number) { - if (expected <= received) { - throw new Error(`Expected \`${expected}\` > \`${received}\`!`); - } -} - -/** - * Asserts that `expected` >= `received`. - * @param expected - * @param received - */ -export function assert_greater_equals(expected: number, received: number) { - if (expected < received) { - throw new Error(`Expected \`${expected}\` >= \`${received}\`!`); +export function assert_equals(expected: any, received: any) { + const fail = () => { + throw new Error(`Expected \`${expected}\`, got \`${received}\`!`); + }; + if (typeof expected !== typeof received) { + fail(); } -} - -/** - * Asserts that `expected` < `received`. - * @param expected - * @param received - */ -export function assert_lesser(expected: number, received: number) { - if (expected >= received) { - throw new Error(`Expected \`${expected}\` < \`${received}\`!`); + // approx checking for floats + if (typeof expected === 'number' && !Number.isInteger(expected)) { + if (Math.abs(expected - received) > 0.001) { + fail(); + } else { + return; + } } -} - -/** - * Asserts that `expected` <= `received`. - * @param expected - * @param received - */ -export function assert_lesser_equals(expected: number, received: number) { - if (expected > received) { - throw new Error(`Expected \`${expected}\` <= \`${received}\`!`); + if (expected !== received) { + fail(); } } From a67e302a3e8a2149ae0326ba4b3535fb19a24fd4 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 9 Apr 2024 02:07:41 +0800 Subject: [PATCH 5/6] Fix auto-fixable lint problems --- src/bundles/testing/__tests__/index.ts | 6 +++--- src/bundles/testing/asserts.ts | 2 +- src/bundles/testing/index.ts | 2 +- src/bundles/testing/list.ts | 4 ++-- src/bundles/testing/types.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bundles/testing/__tests__/index.ts b/src/bundles/testing/__tests__/index.ts index 6a025e2c8..b1da9e134 100644 --- a/src/bundles/testing/__tests__/index.ts +++ b/src/bundles/testing/__tests__/index.ts @@ -1,5 +1,5 @@ -import * as testing from '../functions'; import * as asserts from '../asserts'; +import * as testing from '../functions'; import { list } from '../list'; beforeAll(() => { @@ -13,7 +13,7 @@ beforeAll(() => { testing.context.runtime = 0; }); -test('Test context is created correctly', () => { +test('context is created correctly', () => { const mockTestFn = jest.fn(); testing.describe('Testing 321', () => { testing.it('Testing 123', mockTestFn); @@ -22,7 +22,7 @@ test('Test context is created correctly', () => { expect(mockTestFn).toHaveBeenCalled(); }); -test('Test context fails correctly', () => { +test('context fails correctly', () => { testing.describe('Testing 123', () => { testing.it('This test fails!', () => asserts.assert_equals(0, 1)); }); diff --git a/src/bundles/testing/asserts.ts b/src/bundles/testing/asserts.ts index bacf7920b..75c1531b1 100644 --- a/src/bundles/testing/asserts.ts +++ b/src/bundles/testing/asserts.ts @@ -7,7 +7,7 @@ import { is_pair, head, tail, is_list, is_null, member, length } from './list'; */ export function assert(pred: () => boolean) { if (!pred()) { - throw new Error(`Assert failed!`); + throw new Error('Assert failed!'); } } diff --git a/src/bundles/testing/index.ts b/src/bundles/testing/index.ts index ae85ec80d..9caa27c14 100644 --- a/src/bundles/testing/index.ts +++ b/src/bundles/testing/index.ts @@ -1,4 +1,3 @@ -import { it, describe } from './functions'; import { assert_equals, assert_not_equals, @@ -8,6 +7,7 @@ import { assert_greater_equals, assert_length, } from './asserts'; +import { it, describe } from './functions'; import { mock_fn } from './mocks'; /** diff --git a/src/bundles/testing/list.ts b/src/bundles/testing/list.ts index e5205937c..fdc49241e 100644 --- a/src/bundles/testing/list.ts +++ b/src/bundles/testing/list.ts @@ -13,9 +13,9 @@ // the Firefox environment (especially Web Console) export function array_test(x) : boolean { if (Array.isArray === undefined) { - return x instanceof Array + return x instanceof Array; } else { - return Array.isArray(x) + return Array.isArray(x); } } diff --git a/src/bundles/testing/types.ts b/src/bundles/testing/types.ts index 0f9558c37..0d8102ccf 100644 --- a/src/bundles/testing/types.ts +++ b/src/bundles/testing/types.ts @@ -1,5 +1,5 @@ export type ErrorLogger = ( - error: string | string[], + error: string[] | string, isSlangError?: boolean ) => void; export type Test = () => void; From fbf05c95eccfc912c92c29eb5de4f945601ad89b Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 9 Apr 2024 02:13:17 +0800 Subject: [PATCH 6/6] Fix type import --- src/tabs/Testing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tabs/Testing/index.tsx b/src/tabs/Testing/index.tsx index 62df40cc5..2c9d70ce3 100644 --- a/src/tabs/Testing/index.tsx +++ b/src/tabs/Testing/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Results, SuiteResult } from '../../bundles/testing/types'; +import type { Results, SuiteResult } from '../../bundles/testing/types'; /** * Tab for unit tests.