From 5b65eb5f496e890b6e7d6e0c5527b692ba453656 Mon Sep 17 00:00:00 2001 From: Alphin Tom Date: Fri, 8 Aug 2025 20:46:09 +0200 Subject: [PATCH] test: detect circular formulas --- cli/src/osf.ts | 12 ++++++++++-- cli/tests/cli.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/cli/src/osf.ts b/cli/src/osf.ts index 91ecb7d..33b786f 100644 --- a/cli/src/osf.ts +++ b/cli/src/osf.ts @@ -120,7 +120,7 @@ class FormulaEvaluator { private data: SpreadsheetData; private formulas: Map; private computed: Map; - private evaluating: Set; // For circular reference detection + private evaluating: Set; // For circular reference detection; see cli/tests/cli.test.ts constructor(data: SpreadsheetData, formulas: FormulaDefinition[]) { this.data = { ...data }; @@ -175,7 +175,7 @@ class FormulaEvaluator { getCellValue(row: number, col: number): CellValue { const key = `${row},${col}`; - // Check for circular reference + // Check for circular reference (validated by cli/tests/cli.test.ts) if (this.evaluating.has(key)) { throw new Error(`Circular reference detected at cell ${this.coordsToCellRef(row, col)}`); } @@ -725,6 +725,14 @@ function exportJson(doc: OSFDocument): string { // Get all computed values including formulas const allValues = evaluator.getAllComputedValues(maxRow, maxCol); + // Fail if any formula evaluation resulted in an error (e.g., circular references) + const errorCell = Object.values(allValues).find( + v => typeof v === 'string' && v.startsWith('#ERROR:') + ); + if (typeof errorCell === 'string') { + throw new Error(errorCell.replace('#ERROR: ', '')); + } + // Convert to array format with computed values const computedData = Object.entries(allValues).map(([cell, value]) => { const [r, c] = cell.split(',').map(Number); diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index b0f85a2..3d8bb6f 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -60,6 +60,20 @@ const FORMULA_TEST_OSF = `@meta { formula (2,4): "=C1+C2"; }`; +const CIRCULAR_FORMULA_OSF = `@meta { + title: "Circular Formula Test"; +} + +@sheet { + name: "CycleSheet"; + cols: [A, B]; + data { + (2,1) = 1; + } + formula (1,1): "=B1"; + formula (1,2): "=A1"; +}`; + const INVALID_OSF = `@meta { title: "Unclosed Block" // Missing closing brace`; @@ -377,6 +391,25 @@ describe('OSF CLI', () => { } }); + it('should fail exporting OSF with circular formulas to JSON', () => { + const cycleFile = join(TEST_FIXTURES_DIR, 'cycle_test.osf'); + writeFileSync(cycleFile, CIRCULAR_FORMULA_OSF, 'utf8'); + + try { + const result = execSync(`node "${CLI_PATH}" export "${cycleFile}" --target json`, { + encoding: 'utf8', + }); + expect.fail(`Expected export to fail but succeeded with output: ${result}`); + } catch (err: any) { + const output = (err.stderr || err.stdout) as string; + expect(output).toContain('Circular reference detected'); + } finally { + if (existsSync(cycleFile)) { + unlinkSync(cycleFile); + } + } + }); + it('should export OSF to file', () => { execSync(`node "${CLI_PATH}" export "${testFile}" --output "${outputFile}"`, { encoding: 'utf8',