mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36:02 +08:00
[tests] Improve snapshot testing infrastructure
- test.sh which given a language builds the headless test executable and runs the test with the given inputs - generate-serializers.sh to (re-)generate all language specific serializers - Improved README.md - Removed headless-test-runner.ts, now fully expressed in more concise test.sh
This commit is contained in:
parent
366b965135
commit
0c74907da2
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package com.esotericsoftware.spine.utils;
|
package com.esotericsoftware.spine.utils;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -104,12 +105,7 @@ public class JsonWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String escapeString (String str) {
|
private String escapeString (String str) {
|
||||||
return str.replace("\\", "\\\\")
|
return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\b", "\\b").replace("\f", "\\f").replace("\n", "\\n")
|
||||||
.replace("\"", "\\\"")
|
.replace("\r", "\\r").replace("\t", "\\t");
|
||||||
.replace("\b", "\\b")
|
|
||||||
.replace("\f", "\\f")
|
|
||||||
.replace("\n", "\\n")
|
|
||||||
.replace("\r", "\\r")
|
|
||||||
.replace("\t", "\\t");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
216
tests/README.md
216
tests/README.md
@ -1,142 +1,128 @@
|
|||||||
# Spine Runtimes Test Suite
|
# Spine Runtimes Snapshot Testing
|
||||||
|
|
||||||
This test suite is designed to ensure consistency across all Spine runtime implementations by comparing their outputs against the reference implementation (spine-libgdx).
|
This test suite implements snapshot testing to ensure all Spine runtime implementations produce identical outputs to the Java reference implementation (spine-libgdx).
|
||||||
|
|
||||||
## Purpose
|
## Why Snapshot Testing?
|
||||||
|
|
||||||
Unlike traditional unit tests, this test suite:
|
When porting the Spine runtime to different languages, subtle bugs can be introduced - a constraint might be calculated incorrectly, the order of operations might have been ported incorrectly, or a default value might differ. Traditional unit tests can't catch all these discrepancies.
|
||||||
- Loads skeleton data and animations in each runtime
|
|
||||||
- Outputs all internal state in a consistent, diffable text format
|
|
||||||
- Compares outputs between runtimes to detect discrepancies
|
|
||||||
- Helps maintain consistency when porting changes from the reference implementation
|
|
||||||
|
|
||||||
## HeadlessTest Locations
|
We use snapshot testing to solve this. Each runtime serializes its entire object graph to a standardized format, allowing byte-for-byte comparison between the reference implementation and ports. If outputs differ, we know exactly where the port diverged from the reference.
|
||||||
|
|
||||||
Each runtime has a HeadlessTest program that outputs skeleton data in a standardized format:
|
## The Challenge: Automatic Serializer Generation
|
||||||
|
|
||||||
- **Java (Reference)**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
|
To serialize the complete object graph, we need serializers that output every field and getter from every type in the runtime. Writing these by hand is:
|
||||||
- **C++**: `spine-cpp/tests/HeadlessTest.cpp`
|
- Tedious and error-prone
|
||||||
- **C**: `spine-c/tests/headless-test.c`
|
- Likely to miss fields or getters
|
||||||
- **TypeScript**: `spine-ts/spine-core/tests/HeadlessTest.ts`
|
- Difficult to maintain as the API evolves
|
||||||
|
|
||||||
## Running Individual HeadlessTests
|
We thus implement **automatic serializer generation** using API analysis and code generation.
|
||||||
|
|
||||||
### Java (spine-libgdx)
|
## How It Works
|
||||||
|
|
||||||
|
### 1. API Analysis and core generation
|
||||||
|
We use [`lsp-cli`](https://github.com/badlogic/lsp-cli) to analyze the Java reference implementation:
|
||||||
```bash
|
```bash
|
||||||
cd spine-libgdx
|
./generate-serializers.sh
|
||||||
./gradlew :spine-libgdx-tests:runHeadlessTest -Pargs="<skeleton-path> <atlas-path> [animation-name]"
|
|
||||||
|
|
||||||
# Example with spineboy:
|
|
||||||
./gradlew :spine-libgdx-tests:runHeadlessTest -Pargs="../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### C++ (spine-cpp)
|
This process:
|
||||||
```bash
|
1. **Analyzes Java API** (`analyze-java-api.ts`): Uses Language Server Protocol to discover all types, fields, and getters in spine-libgdx (`tests/output/spine-libgdx-symbols.json`)
|
||||||
cd spine-cpp
|
2. **Generates IR** (`generate-serializer-ir.ts`): Creates an enriched Intermediate Representation with all serialization metadata (`tests/output/analysis-result.json`)
|
||||||
./build.sh # Build if needed
|
3. **Generates Serializers**: Language-specific generators create serializers from the IR:
|
||||||
./build/headless-test <skeleton-path> <atlas-path> [animation-name]
|
- `generate-java-serializer.ts` → `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java`
|
||||||
|
- `generate-cpp-serializer.ts` → `spine-cpp/tests/SkeletonSerializer.h`
|
||||||
|
- C, C#, Dart, Haxe, Swift and TypeScript TBD
|
||||||
|
|
||||||
# Example with spineboy:
|
The IR contains everything needed to generate identical serializers across languages:
|
||||||
./build/headless-test ../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk
|
- Type hierarchies and inheritance chains
|
||||||
|
- All properties (fields and getters) per type
|
||||||
|
- Enum value mappings
|
||||||
|
- Abstract type handling with instanceof chains
|
||||||
|
- Property categorization (primitive, object, array, enum)
|
||||||
|
|
||||||
|
### 2. Snapshot Testing
|
||||||
|
|
||||||
|
Each runtime has a `HeadlessTest` that:
|
||||||
|
1. **Loads** a skeleton file and atlas
|
||||||
|
2. **Creates** a SkeletonData structure and serializes it
|
||||||
|
3. **Constructs** a Skeleton and AnimationState from the SkeletonData
|
||||||
|
4. **Applies** an animation (if specified)
|
||||||
|
5. **Serializes** the resulting Skeleton and AnimationState
|
||||||
|
|
||||||
|
Run tests with:
|
||||||
|
```bash
|
||||||
|
# Test any runtime
|
||||||
|
./test.sh <language> <skeleton-path> <atlas-path> [animation-name]
|
||||||
|
|
||||||
|
# Compare Java vs C++ for spineboy's walk animation
|
||||||
|
./test.sh java ../examples/spineboy/export/spineboy-pro.skel ../examples/spineboy/export/spineboy-pma.atlas walk > java-output.json
|
||||||
|
./test.sh cpp ../examples/spineboy/export/spineboy-pro.skel ../examples/spineboy/export/spineboy-pma.atlas walk > cpp-output.json
|
||||||
|
diff java-output.json cpp-output.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### C (spine-c)
|
Languages: `java`, `cpp`, `c`, `ts`
|
||||||
```bash
|
|
||||||
cd spine-c
|
|
||||||
./build.sh # Build if needed
|
|
||||||
./build/headless-test <skeleton-path> <atlas-path> [animation-name]
|
|
||||||
|
|
||||||
# Example with spineboy:
|
## Debugging Port Failures
|
||||||
./build/headless-test ../examples/spineboy/export/spineboy-pro.json ../examples/spineboy/export/spineboy.atlas walk
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript (spine-ts)
|
When outputs differ, you can pinpoint the exact issue:
|
||||||
```bash
|
|
||||||
cd spine-ts/spine-core
|
|
||||||
npx tsx tests/HeadlessTest.ts <skeleton-path> <atlas-path> [animation-name]
|
|
||||||
|
|
||||||
# Example with spineboy:
|
2. **Value differences**: Different calculations or default values
|
||||||
npx tsx tests/HeadlessTest.ts ../../examples/spineboy/export/spineboy-pro.json ../../examples/spineboy/export/spineboy.atlas walk
|
3. **Animation differences**: Issues in constraint evaluation or animation mixing
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Comparison Test
|
Example: If a transform constraint is ported incorrectly, the skeleton state after animation will differ, showing exactly which bones have wrong transforms. This is a starting point for debugging.
|
||||||
|
|
||||||
The main test runner compares all runtime outputs automatically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./tests/headless-test-runner.ts <skeleton-path> <atlas-path> [animation-name]
|
|
||||||
```
|
|
||||||
|
|
||||||
This script will:
|
|
||||||
1. Check if each runtime's HeadlessTest needs rebuilding
|
|
||||||
2. Build any out-of-date HeadlessTests
|
|
||||||
3. Run each HeadlessTest with the same inputs
|
|
||||||
4. Compare outputs and report any differences
|
|
||||||
5. Save individual outputs to `tests/output/` for manual inspection
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test with spineboy walk animation
|
|
||||||
./tests/headless-test-runner.ts \
|
|
||||||
examples/spineboy/export/spineboy-pro.json \
|
|
||||||
examples/spineboy/export/spineboy-pma.atlas \
|
|
||||||
walk
|
|
||||||
|
|
||||||
# Test without animation (setup pose only)
|
|
||||||
./tests/headless-test-runner.ts \
|
|
||||||
examples/spineboy/export/spineboy-pro.json \
|
|
||||||
examples/spineboy/export/spineboy-pma.atlas
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
Each HeadlessTest outputs:
|
The serializers produce consistent JSON with:
|
||||||
- **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata)
|
- All object properties in deterministic order
|
||||||
- **SKELETON STATE**: Runtime state after applying animations
|
- Floats formatted to 6 decimal places
|
||||||
- **ANIMATION STATE**: Current animation state with tracks and mixing information
|
- Enums as strings
|
||||||
|
- Circular references marked as `"<circular>"`
|
||||||
|
- Type information for every object
|
||||||
|
|
||||||
The output uses consistent JSON formatting:
|
Example output structure:
|
||||||
- Hierarchical structure with 2-space indentation
|
```
|
||||||
- Float values formatted to 6 decimal places
|
=== SKELETON DATA ===
|
||||||
- Strings quoted, nulls explicitly shown
|
{
|
||||||
- Locale-independent number formatting (always uses `.` for decimals)
|
"type": "SkeletonData",
|
||||||
- Circular references marked as `"<circular>"` to prevent infinite recursion
|
"bones": [...],
|
||||||
- Each object includes a `"type"` field for easy identification
|
"slots": [...],
|
||||||
|
...
|
||||||
## Development Tools
|
}
|
||||||
|
=== SKELETON STATE ===
|
||||||
### API Analyzer (Java)
|
{
|
||||||
Analyzes the spine-libgdx API to discover all types and their properties:
|
"type": "Skeleton",
|
||||||
```bash
|
"bones": [...],
|
||||||
cd tests
|
"slots": [...],
|
||||||
npx tsx analyze-java-api.ts
|
...
|
||||||
# Output: output/analysis-result.json
|
}
|
||||||
|
=== ANIMATION STATE ===
|
||||||
|
{
|
||||||
|
"type": "AnimationState",
|
||||||
|
"tracks": [...],
|
||||||
|
...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Serializer Generator (Java)
|
## Project Structure
|
||||||
Generates SkeletonSerializer.java from the analysis:
|
|
||||||
```bash
|
```
|
||||||
cd tests
|
tests/
|
||||||
npx tsx generate-java-serializer.ts
|
├── src/ # TypeScript source files
|
||||||
# Output: ../spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java
|
│ ├── headless-test-runner.ts # Main test runner
|
||||||
|
│ ├── analyze-java-api.ts # Java API analyzer
|
||||||
|
│ ├── generate-serializer-ir.ts # IR generator
|
||||||
|
│ ├── generate-java-serializer.ts # Java serializer generator
|
||||||
|
│ ├── generate-cpp-serializer.ts # C++ serializer generator
|
||||||
|
│ └── types.ts # Shared TypeScript types
|
||||||
|
├── test.sh # Main test script
|
||||||
|
├── generate-serializers.sh # Regenerate all serializers
|
||||||
|
└── output/ # Generated files (gitignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Prompt Generator
|
## HeadlessTest Locations
|
||||||
Generates a prompt for Claude to help port the serializer to other runtimes:
|
|
||||||
```bash
|
|
||||||
cd tests
|
|
||||||
npx tsx generate-claude-prompt.ts
|
|
||||||
# Output: output/port-serializer-prompt.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
- **Java**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
|
||||||
|
- **C++**: `spine-cpp/tests/HeadlessTest.cpp`
|
||||||
If outputs differ between runtimes:
|
- **C**: `spine-c/tests/headless-test.c`
|
||||||
1. Check `tests/output/` for the full outputs from each runtime
|
- **TypeScript**: `spine-ts/spine-core/tests/HeadlessTest.ts`
|
||||||
2. Use a diff tool to compare the files
|
|
||||||
3. Common issues:
|
|
||||||
- Number formatting differences (should be fixed by locale settings)
|
|
||||||
- Missing or extra fields in data structures
|
|
||||||
- Different default values
|
|
||||||
- Rounding differences
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import type { ClassInfo, PropertyInfo } from './types';
|
|
||||||
|
|
||||||
interface SerializedAnalysisResult {
|
|
||||||
classMap: [string, ClassInfo][];
|
|
||||||
accessibleTypes: string[];
|
|
||||||
abstractTypes: [string, string[]][];
|
|
||||||
allTypesToGenerate: string[];
|
|
||||||
typeProperties: [string, PropertyInfo[]][];
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateClaudePrompt(analysisData: SerializedAnalysisResult): string {
|
|
||||||
const output: string[] = [];
|
|
||||||
|
|
||||||
// Convert arrays back to Maps
|
|
||||||
const classMap = new Map(analysisData.classMap);
|
|
||||||
const abstractTypes = new Map(analysisData.abstractTypes);
|
|
||||||
const typeProperties = new Map(analysisData.typeProperties);
|
|
||||||
|
|
||||||
output.push('# Spine Java Serialization Methods to Generate');
|
|
||||||
output.push('');
|
|
||||||
output.push('You need to generate writeXXX() methods for the SkeletonSerializer class.');
|
|
||||||
output.push('Each method should serialize all properties accessible via getter methods.');
|
|
||||||
output.push('');
|
|
||||||
output.push('## Task: Port the Java SkeletonSerializer to Other Runtimes');
|
|
||||||
output.push('');
|
|
||||||
output.push('A complete Java SkeletonSerializer has been generated at:');
|
|
||||||
output.push('`spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java`');
|
|
||||||
output.push('');
|
|
||||||
output.push('Port this serializer to:');
|
|
||||||
output.push('- **C++**: Create `SkeletonSerializer.h/cpp` in `spine-cpp/src/spine/`');
|
|
||||||
output.push('- **C**: Create `spine_skeleton_serializer.h/c` in `spine-c/src/spine/`');
|
|
||||||
output.push('- **TypeScript**: Create `SkeletonSerializer.ts` in `spine-ts/spine-core/src/`');
|
|
||||||
output.push('');
|
|
||||||
output.push('## Important Porting Notes');
|
|
||||||
output.push('');
|
|
||||||
output.push('1. **Language Differences**:');
|
|
||||||
output.push(' - Java uses getter methods (`getName()`), TypeScript may use properties (`.name`)');
|
|
||||||
output.push(' - C uses function calls (`spBoneData_getName()`)');
|
|
||||||
output.push(' - Adapt to each language\'s idioms');
|
|
||||||
output.push('2. **Type Checking**:');
|
|
||||||
output.push(' - Java uses `instanceof`');
|
|
||||||
output.push(' - C++ uses custom RTTI (`object->getRTTI().instanceOf()`)');
|
|
||||||
output.push(' - C uses type fields or function pointers');
|
|
||||||
output.push(' - TypeScript uses `instanceof`');
|
|
||||||
output.push('3. **Collections**:');
|
|
||||||
output.push(' - Java uses `Array<T>`, `IntArray`, `FloatArray`');
|
|
||||||
output.push(' - C++ uses `Vector<T>`');
|
|
||||||
output.push(' - C uses arrays with size fields');
|
|
||||||
output.push(' - TypeScript uses arrays `T[]`');
|
|
||||||
output.push('');
|
|
||||||
output.push('## Types Reference');
|
|
||||||
output.push('');
|
|
||||||
|
|
||||||
// First emit abstract types that need instanceof delegation
|
|
||||||
for (const [className, classInfo] of classMap) {
|
|
||||||
if ((classInfo.isAbstract || classInfo.isInterface) && classInfo.concreteImplementations && classInfo.concreteImplementations.length > 0) {
|
|
||||||
output.push(`### ${className} (${classInfo.isInterface ? 'interface' : 'abstract'})`);
|
|
||||||
if (classInfo.superTypes.length > 0) {
|
|
||||||
output.push(`Extends: ${classInfo.superTypes.join(', ')}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
output.push('This is an abstract class. Generate a write' + className.split('.').pop() + '() method that checks instanceof for these concrete implementations:');
|
|
||||||
for (const impl of classInfo.concreteImplementations.sort()) {
|
|
||||||
output.push(`- ${impl}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
output.push('Example implementation:');
|
|
||||||
output.push('```java');
|
|
||||||
output.push(`private void write${className.split('.').pop()}(JsonWriter json, ${className.split('.').pop()} obj) throws IOException {`);
|
|
||||||
const first = classInfo.concreteImplementations[0];
|
|
||||||
if (first) {
|
|
||||||
const shortName = first.split('.').pop()!;
|
|
||||||
output.push(` if (obj instanceof ${shortName}) {`);
|
|
||||||
output.push(` write${shortName}(json, (${shortName}) obj);`);
|
|
||||||
output.push(' } // ... etc for all concrete types');
|
|
||||||
output.push(' else {');
|
|
||||||
output.push(` throw new RuntimeException("Unknown ${className.split('.').pop()} type: " + obj.getClass().getName());`);
|
|
||||||
output.push(' }');
|
|
||||||
}
|
|
||||||
output.push('}');
|
|
||||||
output.push('```');
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then emit concrete types
|
|
||||||
const sortedTypes = Array.from(analysisData.allTypesToGenerate).sort();
|
|
||||||
|
|
||||||
for (const typeName of sortedTypes) {
|
|
||||||
const classInfo = classMap.get(typeName)!;
|
|
||||||
|
|
||||||
output.push(`### ${typeName}`);
|
|
||||||
if (classInfo && classInfo.superTypes.length > 0) {
|
|
||||||
output.push(`Extends: ${classInfo.superTypes.join(', ')}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
|
|
||||||
const properties = typeProperties.get(typeName) || [];
|
|
||||||
const getters = properties.filter(p => p.isGetter);
|
|
||||||
const fields = properties.filter(p => !p.isGetter);
|
|
||||||
|
|
||||||
if (getters.length > 0 || fields.length > 0) {
|
|
||||||
if (fields.length > 0) {
|
|
||||||
output.push('Public fields:');
|
|
||||||
output.push('```java');
|
|
||||||
for (const field of fields) {
|
|
||||||
output.push(`${field.name} // ${field.type}`);
|
|
||||||
}
|
|
||||||
output.push('```');
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getters.length > 0) {
|
|
||||||
output.push('Getters to serialize:');
|
|
||||||
output.push('```java');
|
|
||||||
for (const getter of getters) {
|
|
||||||
output.push(`${getter.name} // returns ${getter.type}`);
|
|
||||||
}
|
|
||||||
output.push('```');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.push('*No properties found*');
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
// Read analysis result
|
|
||||||
const analysisFile = path.join(process.cwd(), 'output', 'analysis-result.json');
|
|
||||||
if (!fs.existsSync(analysisFile)) {
|
|
||||||
console.error('Analysis result not found. Run analyze-java-api.ts first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
|
|
||||||
|
|
||||||
// Generate Claude prompt
|
|
||||||
const prompt = generateClaudePrompt(analysisData);
|
|
||||||
|
|
||||||
// Write the prompt file
|
|
||||||
const outputFile = path.join(process.cwd(), 'output', 'port-serializer-to-other-runtimes.md');
|
|
||||||
fs.writeFileSync(outputFile, prompt);
|
|
||||||
|
|
||||||
console.log(`Claude prompt written to: ${outputFile}`);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow running as a script or importing the function
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
||||||
main();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { generateClaudePrompt };
|
|
||||||
@ -3,18 +3,19 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR/.."
|
pushd "$SCRIPT_DIR" > /dev/null
|
||||||
|
|
||||||
echo "Analyzing Java API..."
|
echo "Analyzing Java API..."
|
||||||
npx tsx tests/analyze-java-api.ts
|
npx tsx src/analyze-java-api.ts
|
||||||
|
|
||||||
echo "Generating serializer IR..."
|
echo "Generating serializer IR..."
|
||||||
npx tsx tests/generate-serializer-ir.ts
|
npx tsx src/generate-serializer-ir.ts
|
||||||
|
|
||||||
echo "Generating Java SkeletonSerializer..."
|
echo "Generating Java SkeletonSerializer..."
|
||||||
npx tsx tests/generate-java-serializer.ts
|
npx tsx src/generate-java-serializer.ts
|
||||||
|
|
||||||
echo "Generating C++ SkeletonSerializer..."
|
echo "Generating C++ SkeletonSerializer..."
|
||||||
npx tsx tests/generate-cpp-serializer.ts
|
npx tsx src/generate-cpp-serializer.ts
|
||||||
|
|
||||||
echo "Done."
|
echo "Done."
|
||||||
|
popd > /dev/null
|
||||||
@ -1,306 +0,0 @@
|
|||||||
#!/usr/bin/env npx tsx
|
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
const writeFile = promisify(fs.writeFile);
|
|
||||||
const mkdir = promisify(fs.mkdir);
|
|
||||||
const stat = promisify(fs.stat);
|
|
||||||
|
|
||||||
// Helper function to get modification time of a file
|
|
||||||
async function getMTime(filePath: string): Promise<number> {
|
|
||||||
try {
|
|
||||||
const stats = await stat(filePath);
|
|
||||||
return stats.mtimeMs;
|
|
||||||
} catch {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to find newest file in a directory pattern
|
|
||||||
async function getNewestFileTime(baseDir: string, patterns: string[]): Promise<number> {
|
|
||||||
let newest = 0;
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const globPattern = path.join(baseDir, pattern);
|
|
||||||
const files = execSync(`find "${baseDir}" -name "${pattern.split('/').pop()}" -type f 2>/dev/null || true`, {
|
|
||||||
encoding: 'utf8'
|
|
||||||
}).trim().split('\n').filter(f => f);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const mtime = await getMTime(file);
|
|
||||||
if (mtime > newest) newest = mtime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
if (args.length < 2) {
|
|
||||||
console.error('Usage: headless-test-runner.ts <skeleton-path> <atlas-path> [animation-name]');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [skeletonPath, atlasPath, animationName] = args;
|
|
||||||
|
|
||||||
// Get absolute paths
|
|
||||||
const absoluteSkeletonPath = path.resolve(skeletonPath);
|
|
||||||
const absoluteAtlasPath = path.resolve(atlasPath);
|
|
||||||
|
|
||||||
// Script paths
|
|
||||||
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
||||||
const rootDir = path.dirname(scriptDir);
|
|
||||||
const outputDir = path.join(scriptDir, 'output');
|
|
||||||
|
|
||||||
interface RuntimeConfig {
|
|
||||||
name: string;
|
|
||||||
buildCheck: () => Promise<boolean>;
|
|
||||||
build: () => void;
|
|
||||||
run: () => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runtime configurations
|
|
||||||
const runtimes: RuntimeConfig[] = [
|
|
||||||
{
|
|
||||||
name: 'java',
|
|
||||||
buildCheck: async () => {
|
|
||||||
const classPath = path.join(rootDir, 'spine-libgdx/spine-libgdx-tests/build/classes/java/main/com/esotericsoftware/spine/HeadlessTest.class');
|
|
||||||
if (!fs.existsSync(classPath)) return false;
|
|
||||||
|
|
||||||
// Check if any source files are newer than the class file
|
|
||||||
const classTime = await getMTime(classPath);
|
|
||||||
const sourceTime = await getNewestFileTime(
|
|
||||||
path.join(rootDir, 'spine-libgdx'),
|
|
||||||
['spine-libgdx/src/**/*.java', 'spine-libgdx-tests/src/**/*.java']
|
|
||||||
);
|
|
||||||
|
|
||||||
return sourceTime <= classTime;
|
|
||||||
},
|
|
||||||
build: () => {
|
|
||||||
console.log(' Building Java runtime...');
|
|
||||||
execSync('./gradlew :spine-libgdx-tests:build', {
|
|
||||||
cwd: path.join(rootDir, 'spine-libgdx'),
|
|
||||||
stdio: 'inherit'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
run: () => {
|
|
||||||
const args = animationName
|
|
||||||
? `${absoluteSkeletonPath} ${absoluteAtlasPath} ${animationName}`
|
|
||||||
: `${absoluteSkeletonPath} ${absoluteAtlasPath}`;
|
|
||||||
const output = execSync(
|
|
||||||
`./gradlew -q --no-daemon --max-workers=1 :spine-libgdx-tests:runHeadlessTest -Pargs="${args}"`,
|
|
||||||
{
|
|
||||||
cwd: path.join(rootDir, 'spine-libgdx'),
|
|
||||||
encoding: 'utf8',
|
|
||||||
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Find the start of actual output and return everything from there
|
|
||||||
const lines = output.split('\n');
|
|
||||||
const startIndex = lines.findIndex(line => line === '=== SKELETON DATA ===');
|
|
||||||
if (startIndex !== -1) {
|
|
||||||
return lines.slice(startIndex).join('\n').trim();
|
|
||||||
}
|
|
||||||
// Fallback to full output if marker not found
|
|
||||||
return output.trim();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cpp',
|
|
||||||
buildCheck: async () => {
|
|
||||||
const execPath = path.join(rootDir, 'spine-cpp/build/headless-test');
|
|
||||||
if (!fs.existsSync(execPath)) return false;
|
|
||||||
|
|
||||||
// Check if any source files are newer than the executable
|
|
||||||
const execTime = await getMTime(execPath);
|
|
||||||
const sourceTime = await getNewestFileTime(
|
|
||||||
path.join(rootDir, 'spine-cpp'),
|
|
||||||
['spine-cpp/src/**/*.cpp', 'spine-cpp/include/**/*.h', 'tests/DebugPrinter.cpp']
|
|
||||||
);
|
|
||||||
|
|
||||||
return sourceTime <= execTime;
|
|
||||||
},
|
|
||||||
build: () => {
|
|
||||||
console.log(' Building C++ runtime...');
|
|
||||||
execSync('./build.sh clean', {
|
|
||||||
cwd: path.join(rootDir, 'spine-cpp/'),
|
|
||||||
stdio: 'inherit'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
run: () => {
|
|
||||||
return execSync(
|
|
||||||
animationName
|
|
||||||
? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
|
|
||||||
: `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
|
|
||||||
{
|
|
||||||
cwd: path.join(rootDir, 'spine-cpp'),
|
|
||||||
encoding: 'utf8',
|
|
||||||
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
||||||
}
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'c',
|
|
||||||
buildCheck: async () => {
|
|
||||||
const execPath = path.join(rootDir, 'spine-c/build/headless-test');
|
|
||||||
if (!fs.existsSync(execPath)) return false;
|
|
||||||
|
|
||||||
// Check if any source files are newer than the executable
|
|
||||||
const execTime = await getMTime(execPath);
|
|
||||||
const sourceTime = await getNewestFileTime(
|
|
||||||
path.join(rootDir, 'spine-c'),
|
|
||||||
['src/**/*.c', 'include/**/*.h', 'tests/debug-printer.c']
|
|
||||||
);
|
|
||||||
|
|
||||||
return sourceTime <= execTime;
|
|
||||||
},
|
|
||||||
build: () => {
|
|
||||||
console.log(' Building C runtime...');
|
|
||||||
execSync('./build.sh', {
|
|
||||||
cwd: path.join(rootDir, 'spine-c/'),
|
|
||||||
stdio: 'inherit'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
run: () => {
|
|
||||||
return execSync(
|
|
||||||
animationName
|
|
||||||
? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
|
|
||||||
: `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
|
|
||||||
{
|
|
||||||
cwd: path.join(rootDir, 'spine-c'),
|
|
||||||
encoding: 'utf8',
|
|
||||||
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
||||||
}
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ts',
|
|
||||||
buildCheck: async () => {
|
|
||||||
// For TypeScript, just check if the HeadlessTest.ts file exists
|
|
||||||
const headlessTestPath = path.join(rootDir, 'spine-ts/spine-core/tests/HeadlessTest.ts');
|
|
||||||
return fs.existsSync(headlessTestPath);
|
|
||||||
},
|
|
||||||
build: () => {}, // No build needed
|
|
||||||
run: () => {
|
|
||||||
return execSync(
|
|
||||||
animationName
|
|
||||||
? `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
|
|
||||||
: `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
|
|
||||||
{
|
|
||||||
cwd: path.join(rootDir, 'spine-ts/spine-core'),
|
|
||||||
encoding: 'utf8',
|
|
||||||
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
||||||
}
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Clean and recreate output directory
|
|
||||||
if (fs.existsSync(outputDir)) {
|
|
||||||
fs.rmSync(outputDir, { recursive: true });
|
|
||||||
}
|
|
||||||
await mkdir(outputDir, { recursive: true });
|
|
||||||
|
|
||||||
console.log('Comparing HeadlessTest outputs for:');
|
|
||||||
console.log(` Skeleton: ${absoluteSkeletonPath}`);
|
|
||||||
console.log(` Atlas: ${absoluteAtlasPath}`);
|
|
||||||
console.log(` Animation: ${animationName}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Run all runtimes and collect outputs
|
|
||||||
const outputs: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const runtime of runtimes) {
|
|
||||||
console.log(`Running ${runtime.name.toUpperCase()} HeadlessTest...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Build if needed
|
|
||||||
if (!(await runtime.buildCheck())) {
|
|
||||||
runtime.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run and capture output
|
|
||||||
const output = runtime.run();
|
|
||||||
outputs[runtime.name] = output;
|
|
||||||
|
|
||||||
// Save output to file
|
|
||||||
await writeFile(path.join(outputDir, `${runtime.name}.txt`), output);
|
|
||||||
|
|
||||||
console.log(' Done.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(` Error: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
|
|
||||||
outputs[runtime.name] = `Error: ${error instanceof Error ? error.message : JSON.stringify(error)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('Comparing outputs...');
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// Compare outputs
|
|
||||||
const reference = 'java';
|
|
||||||
let allMatch = true;
|
|
||||||
|
|
||||||
for (const runtime of runtimes) {
|
|
||||||
if (runtime.name !== reference) {
|
|
||||||
process.stdout.write(`Comparing ${reference} vs ${runtime.name}: `);
|
|
||||||
|
|
||||||
if (outputs[reference] === outputs[runtime.name]) {
|
|
||||||
console.log('✓ MATCH');
|
|
||||||
} else {
|
|
||||||
console.log('✗ DIFFER');
|
|
||||||
allMatch = false;
|
|
||||||
|
|
||||||
// Show first few differences
|
|
||||||
const refLines = outputs[reference].split('\n');
|
|
||||||
const runtimeLines = outputs[runtime.name].split('\n');
|
|
||||||
const maxLines = Math.max(refLines.length, runtimeLines.length);
|
|
||||||
let diffCount = 0;
|
|
||||||
|
|
||||||
console.log(' First differences:');
|
|
||||||
for (let i = 0; i < maxLines && diffCount < 5; i++) {
|
|
||||||
if (refLines[i] !== runtimeLines[i]) {
|
|
||||||
diffCount++;
|
|
||||||
console.log(` Line ${i + 1}:`);
|
|
||||||
if (refLines[i] !== undefined) {
|
|
||||||
console.log(` - ${refLines[i]}`);
|
|
||||||
}
|
|
||||||
if (runtimeLines[i] !== undefined) {
|
|
||||||
console.log(` + ${runtimeLines[i]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffCount === 0 && refLines.length !== runtimeLines.length) {
|
|
||||||
console.log(` Different number of lines: ${refLines.length} vs ${runtimeLines.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save outputs for manual diff
|
|
||||||
console.log(` Full outputs saved to: ${outputDir}/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
if (allMatch) {
|
|
||||||
console.log('✓ All outputs match!');
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.log(`✗ Outputs differ. Check the output files in ${outputDir}/`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run main function
|
|
||||||
main().catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@ -9,7 +9,7 @@ import type { Symbol, LspOutput, ClassInfo, PropertyInfo, AnalysisResult } from
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
function ensureOutputDir(): string {
|
function ensureOutputDir(): string {
|
||||||
const outputDir = path.resolve(__dirname, '..', 'output');
|
const outputDir = path.resolve(__dirname, '../../output');
|
||||||
if (!fs.existsSync(outputDir)) {
|
if (!fs.existsSync(outputDir)) {
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
@ -84,25 +84,11 @@ function analyzeClasses(symbols: Symbol[]): Map<string, ClassInfo> {
|
|||||||
typeParameters: symbol.typeParameters || []
|
typeParameters: symbol.typeParameters || []
|
||||||
};
|
};
|
||||||
|
|
||||||
// No need to parse superTypes from preview anymore - lsp-cli handles this properly now
|
|
||||||
|
|
||||||
// Check if abstract class
|
// Check if abstract class
|
||||||
if (symbol.preview && symbol.preview.includes('abstract ')) {
|
if (symbol.preview && symbol.preview.includes('abstract ')) {
|
||||||
classInfo.isAbstract = true;
|
classInfo.isAbstract = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log type parameter information if available
|
|
||||||
if (symbol.typeParameters && symbol.typeParameters.length > 0) {
|
|
||||||
console.error(`Class ${className} has type parameters: ${symbol.typeParameters.join(', ')}`);
|
|
||||||
}
|
|
||||||
if (symbol.supertypes) {
|
|
||||||
for (const supertype of symbol.supertypes) {
|
|
||||||
if (supertype.typeArguments && supertype.typeArguments.length > 0) {
|
|
||||||
console.error(` extends ${supertype.name}<${supertype.typeArguments.join(', ')}>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all getter methods, public fields, inner classes, and enum values
|
// Find all getter methods, public fields, inner classes, and enum values
|
||||||
if (symbol.children) {
|
if (symbol.children) {
|
||||||
for (const child of symbol.children) {
|
for (const child of symbol.children) {
|
||||||
@ -270,15 +256,6 @@ function findAccessibleTypes(
|
|||||||
const typeMatches = returnType.match(/\b([A-Z]\w+(?:\.[A-Z]\w+)*)\b/g);
|
const typeMatches = returnType.match(/\b([A-Z]\w+(?:\.[A-Z]\w+)*)\b/g);
|
||||||
if (typeMatches) {
|
if (typeMatches) {
|
||||||
for (const match of typeMatches) {
|
for (const match of typeMatches) {
|
||||||
if (match === 'BoneLocal') {
|
|
||||||
console.error(`Found BoneLocal in return type of ${typeName}`);
|
|
||||||
}
|
|
||||||
if (classMap.has(match) && !visited.has(match)) {
|
|
||||||
toVisit.push(match);
|
|
||||||
if (match === 'BoneLocal') {
|
|
||||||
console.error(`Added BoneLocal to toVisit`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For non-qualified names, also try as inner class
|
// For non-qualified names, also try as inner class
|
||||||
if (!match.includes('.')) {
|
if (!match.includes('.')) {
|
||||||
// Try as inner class of current type and its parents
|
// Try as inner class of current type and its parents
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { SerializerIR, PublicMethod, WriteMethod, Property } from './generate-serializer-ir';
|
import type { Property, SerializerIR } from './generate-serializer-ir';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@ -420,7 +420,7 @@ function generateCppFromIR(ir: SerializerIR): string {
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Read the IR file
|
// Read the IR file
|
||||||
const irFile = path.resolve(__dirname, 'output', 'serializer-ir.json');
|
const irFile = path.resolve(__dirname, '../../output/serializer-ir.json');
|
||||||
if (!fs.existsSync(irFile)) {
|
if (!fs.existsSync(irFile)) {
|
||||||
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
|
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -434,10 +434,7 @@ async function main() {
|
|||||||
// Write the C++ file
|
// Write the C++ file
|
||||||
const cppFile = path.resolve(
|
const cppFile = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'../../../spine-cpp/tests/SkeletonSerializer.h'
|
||||||
'spine-cpp',
|
|
||||||
'tests',
|
|
||||||
'SkeletonSerializer.h'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(cppFile), { recursive: true });
|
fs.mkdirSync(path.dirname(cppFile), { recursive: true });
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { SerializerIR, PublicMethod, WriteMethod, Property } from './generate-serializer-ir';
|
import type { Property, SerializerIR, WriteMethod } from './generate-serializer-ir';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ function generateJavaFromIR(ir: SerializerIR): string {
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Read the IR file
|
// Read the IR file
|
||||||
const irFile = path.resolve(__dirname, 'output', 'serializer-ir.json');
|
const irFile = path.resolve(__dirname, '../../output/serializer-ir.json');
|
||||||
if (!fs.existsSync(irFile)) {
|
if (!fs.existsSync(irFile)) {
|
||||||
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
|
console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -304,15 +304,7 @@ async function main() {
|
|||||||
// Write the Java file
|
// Write the Java file
|
||||||
const javaFile = path.resolve(
|
const javaFile = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'../../../spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java'
|
||||||
'spine-libgdx',
|
|
||||||
'spine-libgdx-tests',
|
|
||||||
'src',
|
|
||||||
'com',
|
|
||||||
'esotericsoftware',
|
|
||||||
'spine',
|
|
||||||
'utils',
|
|
||||||
'SkeletonSerializer.java'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
|
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
|
||||||
@ -539,7 +539,7 @@ function analyzePropertyWithDetails(prop: PropertyInfo, propName: string, getter
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Read analysis result
|
// Read analysis result
|
||||||
const analysisFile = path.resolve(__dirname, '..', 'output', 'analysis-result.json');
|
const analysisFile = path.resolve(__dirname, '../../output/analysis-result.json');
|
||||||
if (!fs.existsSync(analysisFile)) {
|
if (!fs.existsSync(analysisFile)) {
|
||||||
console.error('Analysis result not found. Run analyze-java-api.ts first.');
|
console.error('Analysis result not found. Run analyze-java-api.ts first.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -551,7 +551,7 @@ async function main() {
|
|||||||
const ir = generateSerializerIR(analysisData);
|
const ir = generateSerializerIR(analysisData);
|
||||||
|
|
||||||
// Write the IR file
|
// Write the IR file
|
||||||
const irFile = path.resolve(__dirname, 'output', 'serializer-ir.json');
|
const irFile = path.resolve(__dirname, '../../output/serializer-ir.json');
|
||||||
fs.mkdirSync(path.dirname(irFile), { recursive: true });
|
fs.mkdirSync(path.dirname(irFile), { recursive: true });
|
||||||
fs.writeFileSync(irFile, JSON.stringify(ir, null, 2));
|
fs.writeFileSync(irFile, JSON.stringify(ir, null, 2));
|
||||||
|
|
||||||
@ -1,30 +1,6 @@
|
|||||||
|
import { Symbol, Supertype, LspOutput } from '@mariozechner/lsp-cli';
|
||||||
|
|
||||||
// Shared types for the Spine serializer generator
|
// Shared types for the Spine serializer generator
|
||||||
|
|
||||||
// Match lsp-cli's Supertype interface
|
|
||||||
export interface Supertype {
|
|
||||||
name: string;
|
|
||||||
typeArguments?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match lsp-cli's SymbolInfo interface (we call it Symbol for backward compatibility)
|
|
||||||
export interface Symbol {
|
|
||||||
name: string;
|
|
||||||
kind: string;
|
|
||||||
file: string;
|
|
||||||
preview: string;
|
|
||||||
documentation?: string;
|
|
||||||
typeParameters?: string[];
|
|
||||||
supertypes?: Supertype[];
|
|
||||||
children?: Symbol[];
|
|
||||||
// We don't need range and definition for our use case
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LspOutput {
|
|
||||||
language: string;
|
|
||||||
directory: string;
|
|
||||||
symbols: Symbol[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassInfo {
|
export interface ClassInfo {
|
||||||
className: string;
|
className: string;
|
||||||
superTypes: string[]; // Just the names for backward compatibility
|
superTypes: string[]; // Just the names for backward compatibility
|
||||||
23
tests/test.sh
Executable file
23
tests/test.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Change to the script's directory
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Check if node/npm is available
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
echo "Error: npm is not installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install dependencies if node_modules doesn't exist
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "Installing dependencies..." >&2
|
||||||
|
if ! npm install > /tmp/npm-install.log 2>&1; then
|
||||||
|
echo "npm install failed! Output:" >&2
|
||||||
|
cat /tmp/npm-install.log >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the TypeScript headless test runner with all arguments
|
||||||
|
npx tsx src/headless-test-runner.ts "$@"
|
||||||
@ -12,6 +12,6 @@
|
|||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"*.ts"
|
"src/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user