/** * Expression Parsing Tests * * Tests the expression parsing capabilities of the Evidence DSL, * focusing on operator precedence, associativity, and complex expressions. */ import { DSLCompiler } from '../DSLCompiler.js'; export class ExpressionTests { constructor() { this.arbiter = null; this.compiler = null; this.testResults = []; } setup(arbiter) { this.arbiter = arbiter; this.compiler = new DSLCompiler(arbiter); } runAllTests() { console.log('=== Expression Parsing Tests ===\n'); this.testArithmeticPrecedence(); this.testLogicalPrecedence(); this.testComparisonOperators(); this.testTemporalExpressions(); this.testUnaryOperators(); this.testAttributeAccess(); this.testFunctionCalls(); this.testComplexExpressions(); this.testExpressionErrors(); return this.getTestResults(); } testArithmeticPrecedence() { console.log('Testing Arithmetic Operator Precedence...'); const testCases = [ { input: '1 + 2 * 3', expected: 'Should evaluate as 1 + (2 * 3) = 7', description: 'Multiplication before addition' }, { input: '10 - 3 * 2', expected: 'Should evaluate as 10 - (3 * 2) = 4', description: 'Multiplication before subtraction' }, { input: '8 / 2 * 4', expected: 'Should evaluate as (8 / 2) * 4 = 16', description: 'Left-associative division and multiplication' }, { input: '2 + 3 * 4 - 5', expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9', description: 'Mixed arithmetic with correct precedence' }, { input: '(1 + 2) * 3', expected: 'Should evaluate as (1 + 2) * 3 = 9', description: 'Parentheses override precedence' } ]; testCases.forEach(({ input, expected, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-arithmetic-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Arithmetic precedence test: ${description}`, error); } }); } testLogicalPrecedence() { console.log('Testing Logical Operator Precedence...'); const testCases = [ { input: 'true && false || true', expected: 'Should evaluate as (true && false) || true = true', description: 'AND before OR' }, { input: 'false || true && false', expected: 'Should evaluate as false || (true && false) = false', description: 'AND before OR (alternative)' }, { input: 'NOT true && false', expected: 'Should evaluate as (NOT true) && false = false', description: 'NOT before AND' }, { input: 'true && NOT false', expected: 'Should evaluate as true && (NOT false) = true', description: 'NOT before AND (alternative)' }, { input: '(true || false) && true', expected: 'Should evaluate as (true || false) && true = true', description: 'Parentheses override logical precedence' } ]; testCases.forEach(({ input, expected, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-logical-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Logical precedence test: ${description}`, error); } }); } testComparisonOperators() { console.log('Testing Comparison Operators...'); const testCases = [ { input: '1 == 1', description: 'Equality comparison' }, { input: '1 != 2', description: 'Inequality comparison' }, { input: '5 > 3', description: 'Greater than' }, { input: '3 < 5', description: 'Less than' }, { input: '4 >= 4', description: 'Greater than or equal' }, { input: '4 <= 4', description: 'Less than or equal' }, { input: '1 == 1 && 2 > 1', description: 'Comparison with logical operators' }, { input: '1 + 2 == 3', description: 'Arithmetic in comparison' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-comparison-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Comparison test: ${description}`, error); } }); } testTemporalExpressions() { console.log('Testing Temporal Expressions...'); const testCases = [ { input: 'user.lastActive within 1h', description: 'Temporal within expression' }, { input: 'user.lastLogin within 24h', description: 'Temporal within with hours' }, { input: 'user.createdAt within 7d', description: 'Temporal within with days' }, { input: 'user.lastActivity within 1h && user.isActive', description: 'Temporal with logical operators' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-temporal-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Temporal test: ${description}`, error); } }); } testUnaryOperators() { console.log('Testing Unary Operators...'); const testCases = [ { input: 'NOT true', description: 'NOT operator' }, { input: '!false', description: 'Alternative NOT operator' }, { input: 'NOT (true && false)', description: 'NOT with parenthesized expression' }, { input: 'NOT user.isSuspended', description: 'NOT with attribute access' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-unary-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Unary test: ${description}`, error); } }); } testAttributeAccess() { console.log('Testing Attribute Access...'); const testCases = [ { input: 'user.role', description: 'Simple attribute access' }, { input: 'user.profile.name', description: 'Nested attribute access' }, { input: 'user.permissions[0]', description: 'Array access' }, { input: 'user.role.permissions[0]', description: 'Nested attribute with array access' }, { input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-attribute-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Attribute access test: ${description}`, error); } }); } testFunctionCalls() { console.log('Testing Function Calls...'); const testCases = [ { input: 'hasRole(user, "admin")', description: 'Simple function call' }, { input: 'isMember(user, group)', description: 'Function call with variables' }, { input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments' }, { input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls' }, { input: 'hasRole(user, user.role)', description: 'Function call with attribute access' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-function-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Function call test: ${description}`, error); } }); } testComplexExpressions() { console.log('Testing Complex Expressions...'); const testCases = [ { input: 'user.isActive && (hasRole(user, "admin") || hasPermission(user, resource, "read"))', description: 'Complex logical expression with function calls' }, { input: 'user.balance > 100 && user.isActive && NOT user.isSuspended', description: 'Multiple conditions with NOT' }, { input: 'user.lastActive within 1h && (user.role == "admin" || user.hasEmergencyAccess)', description: 'Temporal with logical conditions' }, { input: 'hasRole(user, "admin") && user.isActive && NOT (user.isSuspended || user.isBlacklisted)', description: 'Complex negation with multiple conditions' }, { input: 'user.score > 0.8 && user.isTrusted && user.lastActivity within 24h', description: 'Multiple attribute conditions with temporal' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-complex-${Date.now()}`); this.assert(result.success, `${description} should parse successfully`); console.log(` ✓ ${description}`); } catch (error) { this.fail(`Complex expression test: ${description}`, error); } }); } testExpressionErrors() { console.log('Testing Expression Error Handling...'); const testCases = [ { input: 'user.role ==', description: 'Incomplete comparison should fail' }, { input: 'user.role &&', description: 'Incomplete logical expression should fail' }, { input: 'hasRole(user,)', description: 'Function call with missing argument should fail' }, { input: 'user.role == "admin" &&', description: 'Incomplete logical expression should fail' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = this.compiler.compile(dsl, `test-error-${Date.now()}`); this.assert(!result.success, `${description} should fail to parse`); console.log(` ✓ ${description} (correctly failed)`); } catch (error) { // Expected to fail console.log(` ✓ ${description} (correctly failed)`); } }); } assert(condition, message) { if (!condition) { throw new Error(`Assertion failed: ${message}`); } } fail(testName, error) { console.log(` ✗ ${testName} failed: ${error.message}`); this.testResults.push({ test: testName, status: 'FAILED', error: error.message }); } getTestResults() { const passed = this.testResults.filter(r => r.status === 'PASSED').length; const failed = this.testResults.filter(r => r.status === 'FAILED').length; const total = this.testResults.length; return { total: total, passed: passed, failed: failed, success: failed === 0, results: this.testResults }; } } export function runExpressionTests(arbiter) { const test = new ExpressionTests(); test.setup(arbiter); return test.runAllTests(); }