From 52775236cc10a3d77d69a0cb8e85e359ea040f7d Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Wed, 31 Dec 2025 13:07:54 +0000 Subject: [PATCH] config: packages,json,shared (5 files) --- .../tests/expressions.cases.json | 107 ++++++++ .../tests/statements.cases.json | 104 ++++++++ .../seed/scripts/runtime/script_executor.d.ts | 241 ++++++++++++++++++ .../shared/seed/scripts/runtime/test_cli.js | 43 ++++ .../seed/scripts/runtime/test_runner.js | 219 ++++++++++++++++ 5 files changed, 714 insertions(+) create mode 100644 packages/json_script_example/tests/expressions.cases.json create mode 100644 packages/json_script_example/tests/statements.cases.json create mode 100644 packages/shared/seed/scripts/runtime/script_executor.d.ts create mode 100644 packages/shared/seed/scripts/runtime/test_cli.js create mode 100644 packages/shared/seed/scripts/runtime/test_runner.js diff --git a/packages/json_script_example/tests/expressions.cases.json b/packages/json_script_example/tests/expressions.cases.json new file mode 100644 index 000000000..b45f687db --- /dev/null +++ b/packages/json_script_example/tests/expressions.cases.json @@ -0,0 +1,107 @@ +{ + "name": "Expression Tests", + "description": "Test all expression types in JSON script", + "tests": [ + { + "name": "Binary expressions - addition", + "function": "all_expressions", + "args": [10, 5], + "expected": { + "sum": 15, + "diff": 5, + "product": 50, + "max": 10, + "bothPositive": true, + "message": "Sum: 15, Product: 50, Max: 10" + } + }, + { + "name": "Binary expressions - negative numbers", + "function": "all_expressions", + "args": [-10, 5], + "expected": { + "sum": -5, + "diff": -15, + "product": -50, + "max": 5, + "bothPositive": false, + "message": "Sum: -5, Product: -50, Max: 5" + } + }, + { + "name": "Binary expressions - equal values", + "function": "all_expressions", + "args": [7, 7], + "expected": { + "sum": 14, + "diff": 0, + "product": 49, + "max": 7, + "bothPositive": true, + "message": "Sum: 14, Product: 49, Max: 7" + } + }, + { + "name": "All operators - basic arithmetic", + "function": "all_operators", + "args": [10, 3], + "expected": { + "arithmetic": { + "add": 13, + "subtract": 7, + "multiply": 30, + "divide": 3.3333333333333335, + "modulo": 1 + }, + "comparison": { + "equal": false, + "notEqual": true, + "lessThan": false, + "greaterThan": true, + "lessOrEqual": false, + "greaterOrEqual": true + }, + "logical": { + "and": 3, + "or": 10, + "not": false + }, + "unary": { + "negate": -10, + "plus": 10 + } + } + }, + { + "name": "All operators - equal values", + "function": "all_operators", + "args": [5, 5], + "expected": { + "arithmetic": { + "add": 10, + "subtract": 0, + "multiply": 25, + "divide": 1, + "modulo": 0 + }, + "comparison": { + "equal": true, + "notEqual": false, + "lessThan": false, + "greaterThan": false, + "lessOrEqual": true, + "greaterOrEqual": true + }, + "logical": { + "and": 5, + "or": 5, + "not": false + }, + "unary": { + "negate": -5, + "plus": 5 + } + } + } + ] +} diff --git a/packages/json_script_example/tests/statements.cases.json b/packages/json_script_example/tests/statements.cases.json new file mode 100644 index 000000000..0f36ac5fb --- /dev/null +++ b/packages/json_script_example/tests/statements.cases.json @@ -0,0 +1,104 @@ +{ + "name": "Statement Tests", + "description": "Test all statement types in JSON script", + "tests": [ + { + "name": "For-each loop with array", + "function": "all_statements", + "args": [[10, 20, 30, 40, 50]], + "expected": { + "count": 5, + "sum": 150, + "average": 30, + "error": null + } + }, + { + "name": "For-each loop with empty array", + "function": "all_statements", + "args": [[]], + "expected": { + "count": 0, + "sum": 0, + "average": 0, + "error": null + } + }, + { + "name": "For-each loop with single item", + "function": "all_statements", + "args": [[100]], + "expected": { + "count": 1, + "sum": 100, + "average": 100, + "error": null + } + }, + { + "name": "Control flow - negative", + "function": "control_flow", + "args": [-5], + "expected": "negative" + }, + { + "name": "Control flow - zero", + "function": "control_flow", + "args": [0], + "expected": "zero" + }, + { + "name": "Control flow - small", + "function": "control_flow", + "args": [5], + "expected": "small" + }, + { + "name": "Control flow - medium", + "function": "control_flow", + "args": [50], + "expected": "medium" + }, + { + "name": "Control flow - large", + "function": "control_flow", + "args": [150], + "expected": "large" + }, + { + "name": "Control flow - boundary (10)", + "function": "control_flow", + "args": [10], + "expected": "medium" + }, + { + "name": "Control flow - boundary (100)", + "function": "control_flow", + "args": [100], + "expected": "large" + }, + { + "name": "Data structures - no parameters", + "function": "data_structures", + "args": [], + "expected": { + "numbers": [1, 2, 3, 4, 5], + "person": { + "name": "John Doe", + "age": 30, + "email": "john@example.com", + "active": true + }, + "config": { + "server": { + "host": "localhost", + "port": 3000, + "protocol": "http" + }, + "features": ["auth", "api", "dashboard"] + }, + "extractedName": "John Doe" + } + } + ] +} diff --git a/packages/shared/seed/scripts/runtime/script_executor.d.ts b/packages/shared/seed/scripts/runtime/script_executor.d.ts new file mode 100644 index 000000000..f50e04653 --- /dev/null +++ b/packages/shared/seed/scripts/runtime/script_executor.d.ts @@ -0,0 +1,241 @@ +/** + * TypeScript type definitions for JSON Script Runtime Executor + */ + +// Expression Types +export type Expression = + | StringLiteral + | NumberLiteral + | BooleanLiteral + | NullLiteral + | TemplateLiteral + | BinaryExpression + | LogicalExpression + | UnaryExpression + | ConditionalExpression + | MemberAccess + | CallExpression + | ObjectLiteral + | ArrayLiteral + | Reference; + +export interface StringLiteral { + type: 'string_literal'; + value: string; +} + +export interface NumberLiteral { + type: 'number_literal'; + value: number; +} + +export interface BooleanLiteral { + type: 'boolean_literal'; + value: boolean; +} + +export interface NullLiteral { + type: 'null_literal'; +} + +export interface TemplateLiteral { + type: 'template_literal'; + template: string; +} + +export interface BinaryExpression { + type: 'binary_expression'; + left: Expression | any; + operator: '+' | '-' | '*' | '/' | '%' | '==' | '===' | '!=' | '!==' | '<' | '>' | '<=' | '>='; + right: Expression | any; +} + +export interface LogicalExpression { + type: 'logical_expression'; + left: Expression | any; + operator: '&&' | '||' | '??' | 'and' | 'or'; + right: Expression | any; +} + +export interface UnaryExpression { + type: 'unary_expression'; + operator: '!' | '-' | '+' | '~' | 'not' | 'typeof'; + argument: Expression | any; +} + +export interface ConditionalExpression { + type: 'conditional_expression'; + test: Expression | any; + consequent: Expression | any; + alternate: Expression | any; +} + +export interface MemberAccess { + type: 'member_access'; + object: Expression | any; + property: string | Expression | any; + computed?: boolean; + optional?: boolean; +} + +export interface CallExpression { + type: 'call_expression'; + callee: string | Expression; + args?: Array; +} + +export interface ObjectLiteral { + type: 'object_literal'; + properties: Record; +} + +export interface ArrayLiteral { + type: 'array_literal'; + elements: Array; +} + +export type Reference = string; // $ref:path.to.value + +// Statement Types +export type Statement = + | ConstDeclaration + | LetDeclaration + | Assignment + | IfStatement + | ReturnStatement + | TryCatch + | CallExpressionStatement + | ForEachLoop + | Comment; + +export interface ConstDeclaration { + type: 'const_declaration'; + name: string; + value: Expression | any; +} + +export interface LetDeclaration { + type: 'let_declaration'; + name: string; + value: Expression | any; +} + +export interface Assignment { + type: 'assignment'; + target: string | Reference; + value: Expression | any; +} + +export interface IfStatement { + type: 'if_statement'; + condition: Expression | any; + then?: Statement[]; + else?: Statement[]; +} + +export interface ReturnStatement { + type: 'return'; + value: Expression | any; +} + +export interface TryCatch { + type: 'try_catch'; + try: Statement[]; + catch?: { + param?: string; + body: Statement[]; + }; + finally?: Statement[]; +} + +export interface CallExpressionStatement { + type: 'call_expression'; + callee: string | Expression; + args?: Array; +} + +export interface ForEachLoop { + type: 'for_each_loop'; + iterator: string; + iterable: Expression | any; + body?: Statement[]; +} + +export interface Comment { + type: 'comment'; + text: string; +} + +// Function Definition +export interface FunctionParameter { + name: string; + type: string; + description?: string; + optional?: boolean; + default?: any; +} + +export interface FunctionDefinition { + id: string; + name: string; + description?: string; + async?: boolean; + exported?: boolean; + params?: FunctionParameter[]; + returnType?: string; + body: Statement[]; +} + +// Constant Definition +export interface ConstantDefinition { + id: string; + name: string; + type: string; + value: any; + description?: string; + exported?: boolean; +} + +// Script JSON Structure +export interface ScriptJSON { + schema_version: string; + package: string; + description?: string; + constants?: ConstantDefinition[]; + functions: FunctionDefinition[]; +} + +// Execution Context +export interface ExecutionContext { + params: Record; + local_vars: Record; + constants: Record; + imports: Record; + functions: Record; + catch?: Record; +} + +// Return Value +export interface ReturnValue { + type: 'return'; + value: any; +} + +// Main API +export function resolveRef(ref: any, context: ExecutionContext): any; +export function evalExpression(expr: Expression | any, context: ExecutionContext): any; +export function executeStatement(stmt: Statement, context: ExecutionContext): ReturnValue | null; +export function executeFunction(scriptJson: ScriptJSON, functionName: string, args?: any[]): any; +export function loadAndExecute(jsonPath: string, functionName: string, args?: any[]): Promise; +export function loadAndExecuteSync(jsonPath: string, functionName: string, args?: any[]): any; + +declare const _default: { + executeFunction: typeof executeFunction; + loadAndExecute: typeof loadAndExecute; + loadAndExecuteSync: typeof loadAndExecuteSync; + resolveRef: typeof resolveRef; + evalExpression: typeof evalExpression; + executeStatement: typeof executeStatement; +}; + +export default _default; diff --git a/packages/shared/seed/scripts/runtime/test_cli.js b/packages/shared/seed/scripts/runtime/test_cli.js new file mode 100644 index 000000000..fe32930d5 --- /dev/null +++ b/packages/shared/seed/scripts/runtime/test_cli.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/** + * CLI Test Runner for JSON Script Tests + * Usage: node test_cli.js + */ + +const { runTestSuiteSync, formatResults } = require('./test_runner.js'); +const path = require('path'); + +function main() { + const args = process.argv.slice(2); + + if (args.length < 2) { + console.error('Usage: node test_cli.js '); + console.error(''); + console.error('Example:'); + console.error(' node test_cli.js ../../script.json ../../../json_script_example/tests/expressions.cases.json'); + process.exit(1); + } + + const scriptPath = path.resolve(args[0]); + const testPath = path.resolve(args[1]); + + console.log('Running tests...'); + console.log(`Script: ${scriptPath}`); + console.log(`Test Suite: ${testPath}`); + + try { + const results = runTestSuiteSync(scriptPath, testPath); + const output = formatResults(results); + console.log(output); + + // Exit with error code if tests failed + process.exit(results.failed > 0 ? 1 : 0); + } catch (error) { + console.error('\nāŒ Test execution failed:'); + console.error(error.message); + console.error(error.stack); + process.exit(1); + } +} + +main(); diff --git a/packages/shared/seed/scripts/runtime/test_runner.js b/packages/shared/seed/scripts/runtime/test_runner.js new file mode 100644 index 000000000..3ca207632 --- /dev/null +++ b/packages/shared/seed/scripts/runtime/test_runner.js @@ -0,0 +1,219 @@ +/** + * JSON-based Test Runner for Script Executor + * Executes test cases defined in JSON format + */ + +import { executeFunction } from './script_executor.js'; + +/** + * Deep equality comparison + */ +function deepEqual(a, b) { + if (a === b) return true; + if (a === null || b === null) return false; + if (typeof a !== typeof b) return false; + if (typeof a !== 'object') return false; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!keysB.includes(key)) return false; + if (!deepEqual(a[key], b[key])) return false; + } + + return true; +} + +/** + * Execute a single test case + * @param {Object} testCase - Test case definition + * @param {Object} scriptJson - Script JSON to test against + * @returns {Object} Test result + */ +function executeTestCase(testCase, scriptJson) { + const startTime = performance.now(); + let passed = false; + let actualResult = null; + let error = null; + + try { + actualResult = executeFunction(scriptJson, testCase.function, testCase.args || []); + + // Check result + if (testCase.expected !== undefined) { + passed = deepEqual(actualResult, testCase.expected); + } else if (testCase.expectedError) { + passed = false; // Should have thrown + } else { + passed = true; // No assertion + } + } catch (err) { + error = err; + if (testCase.expectedError) { + // Check if error matches expected + passed = err.message.includes(testCase.expectedError); + } else { + passed = false; + } + } + + const duration = performance.now() - startTime; + + return { + name: testCase.name, + function: testCase.function, + passed, + duration, + actualResult, + expectedResult: testCase.expected, + error: error ? error.message : null + }; +} + +/** + * Execute test suite from JSON + * @param {Object} testSuite - Test suite definition + * @param {Object} scriptJson - Script JSON to test + * @returns {Object} Test results + */ +function executeTestSuite(testSuite, scriptJson) { + const results = { + suite: testSuite.name || 'Unnamed Suite', + description: testSuite.description, + total: 0, + passed: 0, + failed: 0, + duration: 0, + tests: [] + }; + + const startTime = performance.now(); + + for (const testCase of (testSuite.tests || [])) { + const result = executeTestCase(testCase, scriptJson); + results.tests.push(result); + results.total++; + if (result.passed) { + results.passed++; + } else { + results.failed++; + } + } + + results.duration = performance.now() - startTime; + + return results; +} + +/** + * Format test results as human-readable string + * @param {Object} results - Test results + * @returns {string} Formatted output + */ +function formatResults(results) { + const lines = []; + lines.push(`\n${'='.repeat(60)}`); + lines.push(`Test Suite: ${results.suite}`); + if (results.description) { + lines.push(`Description: ${results.description}`); + } + lines.push(`${'='.repeat(60)}\n`); + + for (const test of results.tests) { + const icon = test.passed ? 'āœ…' : 'āŒ'; + lines.push(`${icon} ${test.name}`); + lines.push(` Function: ${test.function}`); + lines.push(` Duration: ${test.duration.toFixed(2)}ms`); + + if (!test.passed) { + lines.push(` Expected: ${JSON.stringify(test.expectedResult)}`); + lines.push(` Actual: ${JSON.stringify(test.actualResult)}`); + if (test.error) { + lines.push(` Error: ${test.error}`); + } + } + lines.push(''); + } + + lines.push(`${'='.repeat(60)}`); + lines.push(`Results: ${results.passed}/${results.total} passed (${results.failed} failed)`); + lines.push(`Total Duration: ${results.duration.toFixed(2)}ms`); + lines.push(`${'='.repeat(60)}\n`); + + if (results.failed === 0) { + lines.push('šŸŽ‰ All tests passed!'); + } else { + lines.push(`āŒ ${results.failed} test(s) failed.`); + } + + return lines.join('\n'); +} + +/** + * Load and run test suite from files + * @param {string} scriptPath - Path to script.json + * @param {string} testPath - Path to test suite JSON + * @returns {Promise} Test results + */ +async function runTestSuite(scriptPath, testPath) { + const fs = await import('fs/promises'); + + const scriptContent = await fs.readFile(scriptPath, 'utf-8'); + const scriptJson = JSON.parse(scriptContent); + + const testContent = await fs.readFile(testPath, 'utf-8'); + const testSuite = JSON.parse(testContent); + + return executeTestSuite(testSuite, scriptJson); +} + +/** + * Synchronous version for Node.js + */ +function runTestSuiteSync(scriptPath, testPath) { + if (typeof require !== 'undefined') { + const fs = require('fs'); + + const scriptContent = fs.readFileSync(scriptPath, 'utf-8'); + const scriptJson = JSON.parse(scriptContent); + + const testContent = fs.readFileSync(testPath, 'utf-8'); + const testSuite = JSON.parse(testContent); + + return executeTestSuite(testSuite, scriptJson); + } + throw new Error('Synchronous file loading not available in browser environment'); +} + +// Export for different module systems +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + executeTestCase, + executeTestSuite, + formatResults, + runTestSuite, + runTestSuiteSync, + deepEqual + }; +} + +export { + executeTestCase, + executeTestSuite, + formatResults, + runTestSuite, + runTestSuiteSync, + deepEqual +}; + +export default { + executeTestCase, + executeTestSuite, + formatResults, + runTestSuite, + runTestSuiteSync, + deepEqual +};