256 lines
6.8 KiB
TypeScript
256 lines
6.8 KiB
TypeScript
import { spawnSync } from 'child_process';
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
interface AssertPythonIsValidInput {
|
|
files: {
|
|
[filename: string]: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Asserts that the provided Python code is syntactically valid.
|
|
*/
|
|
export function assertPythonIsValid({ files }: AssertPythonIsValidInput) {
|
|
for (const [filename, code] of Object.entries(files)) {
|
|
try {
|
|
// Use spawnSync to avoid shell quote escaping issues by passing code via stdin
|
|
const {
|
|
status: exitCode,
|
|
stderr,
|
|
stdout,
|
|
} = spawnSync(
|
|
'python3',
|
|
['-c', 'import sys; compile(sys.stdin.read(), "<string>", "exec")'],
|
|
{
|
|
input: code,
|
|
encoding: 'utf8',
|
|
}
|
|
);
|
|
|
|
if (exitCode !== 0) {
|
|
throw new Error(stderr || stdout || 'Python compilation failed');
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
throw new Error(`Invalid Python code in ${filename}:\n${error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (import.meta.vitest) {
|
|
describe('assertPythonIsValid', () => {
|
|
it('[Given] valid Python code [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
def greet(name):
|
|
return f"Hello, {name}!"
|
|
|
|
result = greet("World")
|
|
print(result)
|
|
`;
|
|
assertPythonIsValid({ files: { 'main.py': code } });
|
|
});
|
|
|
|
it('[Given] valid Python code with imports [Then] no errors are found', () => {
|
|
const mainSource = /* python */ `
|
|
import math
|
|
|
|
def calculate_area(radius):
|
|
return math.pi * radius ** 2
|
|
|
|
area = calculate_area(5)
|
|
print(f"Area: {area}")
|
|
`;
|
|
|
|
assertPythonIsValid({
|
|
files: { 'main.py': mainSource },
|
|
});
|
|
});
|
|
|
|
it('[Given] valid Python code with class definition [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
class Calculator:
|
|
def __init__(self):
|
|
self.result = 0
|
|
|
|
def add(self, x, y):
|
|
self.result = x + y
|
|
return self.result
|
|
|
|
calc = Calculator()
|
|
calc.add(2, 3)
|
|
`;
|
|
assertPythonIsValid({ files: { 'calculator.py': code } });
|
|
});
|
|
|
|
it('[Given] valid Python code with string escaping [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
message = "Hello \\"World\\" with 'quotes'"
|
|
template = '''This is a multiline
|
|
string with "double" and 'single' quotes'''
|
|
formatted = f"Template: {template}"
|
|
print(formatted)
|
|
`;
|
|
assertPythonIsValid({ files: { 'strings.py': code } });
|
|
});
|
|
|
|
it('[Given] invalid Python code [Then] errors are found', () => {
|
|
const code = /* python */ `
|
|
def broken_function(
|
|
print("This is missing a closing parenthesis"
|
|
`;
|
|
expect(() => {
|
|
assertPythonIsValid({ files: { 'broken.py': code } });
|
|
}).toThrowError();
|
|
});
|
|
|
|
it('[Given] Python code with syntax error [Then] errors are found', () => {
|
|
const code = /* python */ `
|
|
def invalid_syntax():
|
|
if True
|
|
print("Missing colon")
|
|
`;
|
|
expect(() => {
|
|
assertPythonIsValid({ files: { 'syntax_error.py': code } });
|
|
}).toThrowError();
|
|
});
|
|
|
|
it('[Given] Python code with indentation error [Then] errors are found', () => {
|
|
const code = /* python */ `
|
|
def bad_indentation():
|
|
x = 1
|
|
y = 2 # Wrong indentation
|
|
return x + y
|
|
`;
|
|
expect(() => {
|
|
assertPythonIsValid({ files: { 'indentation_error.py': code } });
|
|
}).toThrowError();
|
|
});
|
|
|
|
it('[Given] multiple valid Python files [Then] no errors are found', () => {
|
|
const mainSource = /* python */ `
|
|
from math_utils import add, multiply
|
|
|
|
def main():
|
|
result = multiply(add(2, 3), 4)
|
|
print(f"Result: {result}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
`;
|
|
|
|
const mathUtilsSource = /* python */ `
|
|
def add(x, y):
|
|
return x + y
|
|
|
|
def multiply(x, y):
|
|
return x * y
|
|
`;
|
|
|
|
assertPythonIsValid({
|
|
files: { 'main.py': mainSource, 'math_utils.py': mathUtilsSource },
|
|
});
|
|
});
|
|
|
|
it('[Given] valid Python code with decorators [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
def my_decorator(func):
|
|
def wrapper(*args, **kwargs):
|
|
print(f"Calling {func.__name__}")
|
|
return func(*args, **kwargs)
|
|
return wrapper
|
|
|
|
@my_decorator
|
|
def greet(name):
|
|
return f"Hello, {name}!"
|
|
|
|
result = greet("World")
|
|
print(result)
|
|
`;
|
|
assertPythonIsValid({ files: { 'decorators.py': code } });
|
|
});
|
|
|
|
it('[Given] valid Python code with async/await [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
import asyncio
|
|
|
|
async def fetch_data():
|
|
await asyncio.sleep(1)
|
|
return "data"
|
|
|
|
async def main():
|
|
result = await fetch_data()
|
|
print(result)
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
`;
|
|
assertPythonIsValid({ files: { 'async_example.py': code } });
|
|
});
|
|
|
|
it('[Given] valid Python code with type hints [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
from typing import List, Optional, Dict
|
|
|
|
def process_items(items: List[str], config: Optional[Dict[str, bool]] = None) -> str:
|
|
if config is None:
|
|
config = {}
|
|
|
|
processed = [item.upper() for item in items if config.get("uppercase", True)]
|
|
return ", ".join(processed)
|
|
|
|
result: str = process_items(["hello", "world"])
|
|
print(result)
|
|
`;
|
|
assertPythonIsValid({ files: { 'type_hints.py': code } });
|
|
});
|
|
|
|
it('[Given] Python code with complex string literals [Then] no errors are found', () => {
|
|
const code = /* python */ `
|
|
# Test various string literal formats
|
|
single_quotes = 'This is a single-quoted string'
|
|
double_quotes = "This is a double-quoted string"
|
|
triple_single = '''This is a
|
|
multi-line string
|
|
with single quotes'''
|
|
triple_double = """This is a
|
|
multi-line string
|
|
with double quotes"""
|
|
|
|
# Raw strings
|
|
raw_string = r"This is a raw string with \\n and \\t"
|
|
f_string = f"Hello, {single_quotes}"
|
|
|
|
# Mixed quotes
|
|
mixed = "She said, 'Hello there!' to him"
|
|
escaped = "This has \\"escaped\\" quotes"
|
|
|
|
print(f_string)
|
|
`;
|
|
assertPythonIsValid({ files: { 'string_literals.py': code } });
|
|
});
|
|
|
|
it('[Given] invalid Python code with undefined variable [Then] errors are found', () => {
|
|
const code = /* python */ `
|
|
def test_function():
|
|
return undefined_variable
|
|
`;
|
|
// Note: Python syntax validation won't catch undefined variables - only syntax errors
|
|
// This should pass syntax validation but would fail at runtime
|
|
assertPythonIsValid({ files: { 'undefined_var.py': code } });
|
|
});
|
|
|
|
it('[Given] Python code with syntax error in function call [Then] errors are found', () => {
|
|
const code = /* python */ `
|
|
def test_function():
|
|
print("Hello",) # This trailing comma is valid
|
|
print("Hello",,) # This double comma is invalid
|
|
`;
|
|
expect(() => {
|
|
assertPythonIsValid({ files: { 'function_call_error.py': code } });
|
|
}).toThrowError();
|
|
});
|
|
});
|
|
}
|