mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
* fix: consolidate C/C++/C#/Rust language support from 6 overlapping PRs Merges fixes from PRs #163, #170, #178, #216, #227, #234 into a single coherent changeset with shared modules and deduplication. Phase 0 — Pre-merge consolidation: - Extract isNodeExported to shared export-detection.ts module - Extract TREE_SITTER_BUFFER_SIZE to shared constants.ts with adaptive sizing - Consolidate FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise from duplicated call-processor.ts and parse-worker.ts into shared utils.ts - Add query compilation smoke tests for all 12 languages Language fixes: - fix(c/cpp): isExported checks static linkage instead of returning false - fix(c/cpp): .h files parsed as C++ (tree-sitter-cpp is superset of C) - fix(c/cpp): expanded entry point patterns (~30 new for C, ~18 for C++) - fix(cpp): add typedef, union, macro, prototype, inline method queries - fix(c#): isExported scans sibling modifiers instead of parent walk - fix(c#): heritage queries use correct base_list AST structure - fix(c#): add framework detection, import resolution, entry point scoring - fix(rust): isExported scans sibling visibility_modifier in declaration - fix(builtins): remove open/read/write/close (real C POSIX syscalls) - fix(buffer): adaptive bufferSize (2x fileSize, 512KB-32MB range) - feat(ts/js): add call_expression query patterns for const assignments Deduplication: - call-processor.ts: -226 lines (uses shared utils) - parse-worker.ts: -320 lines (uses shared utils) - parsing-processor.ts: -156 lines (uses shared export-detection) * perf: fix review findings — hoist Sets, deduplicate DEFINITION_CAPTURE_KEYS - Hoist CSHARP_DECL_TYPES and RUST_DECL_TYPES to module-level constants in export-detection.ts (was allocating new Set on every isNodeExported call) - Extract DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures to shared utils.ts (was duplicated in parsing-processor.ts and parse-worker.ts) - Pre-compute merged entry point patterns to avoid per-call array spread in calculateEntryPointScore * test: add C, C++, and Tree-sitter buffer size tests * fix: C/C++/Rust review findings + comprehensive test coverage (+72 tests) Source fixes: - Add Rust built-in noise (unwrap, clone, into, collect, panic, etc.) - C++ anonymous namespace → internal linkage (not exported) - Replace .text regex with storage_class_specifier child scan (perf) - Raise file skip threshold from 512KB to 32MB (TREE_SITTER_MAX_BUFFER) - Export TREE_SITTER_MAX_BUFFER from constants.ts - Add C++ double pointer query patterns to CPP_QUERIES - Add C#: record_struct, record_class, file_scoped_namespace to decl types - Add Rust: union_item to visibility scanning set Tests (214 → 286): - ingestion-utils: +24 (Rust/C# noise, pointer/ref/destructor extraction, buffer) - parsing: +36 (real AST C/C++ static/namespace, Rust/C#/Java/PHP/Swift edge cases) - tree-sitter-languages: +12 (query accuracy for C/C++/C#/Rust captures)
447 lines
18 KiB
TypeScript
447 lines
18 KiB
TypeScript
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { LANGUAGE_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
import { getLanguageFromFilename } from '../../src/core/ingestion/utils.js';
|
|
import Parser from 'tree-sitter';
|
|
|
|
const fixturesDir = path.resolve(__dirname, '..', 'fixtures', 'sample-code');
|
|
|
|
function readFixture(filename: string): string {
|
|
return fs.readFileSync(path.join(fixturesDir, filename), 'utf-8');
|
|
}
|
|
|
|
function parseAndQuery(parser: Parser, content: string, queryStr: string) {
|
|
const tree = parser.parse(content);
|
|
const lang = parser.getLanguage();
|
|
const query = new Parser.Query(lang, queryStr);
|
|
const matches = query.matches(tree.rootNode);
|
|
return { tree, matches };
|
|
}
|
|
|
|
function extractDefinitions(matches: any[]) {
|
|
const defs: { type: string; name: string }[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name === 'name' && match.captures.some((c: any) =>
|
|
c.name.startsWith('definition.'))) {
|
|
const defType = match.captures.find((c: any) => c.name.startsWith('definition.'))!.name;
|
|
defs.push({ type: defType, name: capture.node.text });
|
|
}
|
|
}
|
|
}
|
|
return defs;
|
|
}
|
|
|
|
describe('Tree-sitter multi-language parsing', () => {
|
|
let parser: Parser;
|
|
|
|
beforeAll(async () => {
|
|
parser = await loadParser();
|
|
});
|
|
|
|
describe('TypeScript', () => {
|
|
it('parses functions, classes, interfaces, methods, and arrow functions', async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'simple.ts');
|
|
const content = readFixture('simple.ts');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
expect(defTypes).toContain('definition.function');
|
|
});
|
|
});
|
|
|
|
describe('TSX', () => {
|
|
it('parses JSX components with tsx grammar', async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'simple.tsx');
|
|
const content = readFixture('simple.tsx');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
// Should detect Counter class and Button/useCounter functions
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('Counter');
|
|
});
|
|
});
|
|
|
|
describe('JavaScript', () => {
|
|
it('parses class and function declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.JavaScript);
|
|
const content = readFixture('simple.js');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.JavaScript]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('EventEmitter');
|
|
expect(names).toContain('createLogger');
|
|
});
|
|
});
|
|
|
|
describe('Python', () => {
|
|
it('parses class and function definitions', async () => {
|
|
await loadLanguage(SupportedLanguages.Python);
|
|
const content = readFixture('simple.py');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Python]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
expect(defTypes).toContain('definition.function');
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
it('parses class, method, and constructor declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.Java);
|
|
const content = readFixture('simple.java');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Java]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
expect(defTypes).toContain('definition.method');
|
|
});
|
|
});
|
|
|
|
describe('Go', () => {
|
|
it('parses function and type declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.Go);
|
|
const content = readFixture('simple.go');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Go]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.function');
|
|
});
|
|
});
|
|
|
|
describe('C', () => {
|
|
it('parses function definitions and structs', async () => {
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const content = readFixture('simple.c');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.C]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.function');
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('add');
|
|
expect(names).toContain('internal_helper');
|
|
expect(names).toContain('print_message');
|
|
});
|
|
|
|
it('captures pointer-returning function definitions', async () => {
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const code = `int* get_ptr() { return 0; }\nchar** get_strs() { return 0; }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.C]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('get_ptr');
|
|
expect(names).toContain('get_strs');
|
|
});
|
|
|
|
it('captures macros and typedefs', async () => {
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const code = `#define MAX_SIZE 100\ntypedef unsigned int uint;\nstruct Point { int x; int y; };`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.C]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('MAX_SIZE');
|
|
expect(names).toContain('uint');
|
|
expect(names).toContain('Point');
|
|
});
|
|
});
|
|
|
|
describe('C++', () => {
|
|
it('parses class, function, and namespace declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const content = readFixture('simple.cpp');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('UserManager');
|
|
expect(names).toContain('helperFunction');
|
|
});
|
|
|
|
it('captures pointer-returning methods and functions', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `int* Factory::create() { return nullptr; }\nchar** getNames() { return 0; }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('create');
|
|
expect(names).toContain('getNames');
|
|
});
|
|
|
|
it('captures reference-returning functions', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `int& Container::at(int i) { static int x; return x; }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('at');
|
|
});
|
|
|
|
it('captures destructor definitions', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `MyClass::~MyClass() { cleanup(); }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('~MyClass');
|
|
});
|
|
|
|
it('captures template declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `template<typename T> class Container { T value; };`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('Container');
|
|
});
|
|
|
|
it('captures namespace definitions', async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = `namespace utils { void helper() {} }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CPlusPlus]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('utils');
|
|
expect(names).toContain('helper');
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('parses class, method, and namespace declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
const content = readFixture('simple.cs');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
expect(defTypes).toContain('definition.method');
|
|
expect(defTypes).toContain('definition.namespace');
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('Calculator');
|
|
expect(names).toContain('Add');
|
|
});
|
|
|
|
it('captures interfaces, enums, records, structs', async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
const content = readFixture('simple.cs');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('ICalculator');
|
|
expect(names).toContain('Operation');
|
|
expect(names).toContain('CalculationResult');
|
|
expect(names).toContain('Point');
|
|
});
|
|
|
|
it('captures file-scoped namespace declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
const code = `namespace MyApp;\npublic class Program { }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.CSharp]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('MyApp');
|
|
expect(names).toContain('Program');
|
|
});
|
|
|
|
it('captures constructors and properties', async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
const content = readFixture('simple.cs');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.CSharp]);
|
|
const defs = extractDefinitions(matches);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.constructor');
|
|
expect(defTypes).toContain('definition.property');
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('parses fn, struct, impl, trait, and enum', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const content = readFixture('simple.rs');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.function');
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('public_function');
|
|
expect(names).toContain('private_function');
|
|
expect(names).toContain('Config');
|
|
});
|
|
|
|
it('captures impl blocks and methods', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const content = readFixture('simple.rs');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
|
|
const defs = extractDefinitions(matches);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.impl');
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('new');
|
|
});
|
|
|
|
it('captures generic impl blocks', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const code = `struct Vec<T> { data: Vec<T> }\nimpl<T> Vec<T> { fn len(&self) -> usize { 0 } }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('Vec');
|
|
});
|
|
|
|
it('captures trait impl heritage', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const code = `trait Display { fn fmt(&self); }\nstruct Foo;\nimpl Display for Foo { fn fmt(&self) {} }`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
|
|
// Look for heritage captures
|
|
const heritageCaptures: string[] = [];
|
|
for (const match of matches) {
|
|
for (const capture of match.captures) {
|
|
if (capture.name.startsWith('heritage.')) {
|
|
heritageCaptures.push(`${capture.name}:${capture.node.text}`);
|
|
}
|
|
}
|
|
}
|
|
expect(heritageCaptures).toContain('heritage.trait:Display');
|
|
expect(heritageCaptures).toContain('heritage.class:Foo');
|
|
});
|
|
|
|
it('captures modules, consts, and statics', async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
const code = `mod utils { pub fn helper() {} }\npub const MAX: usize = 100;\nstatic INSTANCE: i32 = 0;`;
|
|
const { matches } = parseAndQuery(parser, code, LANGUAGE_QUERIES[SupportedLanguages.Rust]);
|
|
const defs = extractDefinitions(matches);
|
|
const names = defs.map(d => d.name);
|
|
expect(names).toContain('utils');
|
|
expect(names).toContain('MAX');
|
|
expect(names).toContain('INSTANCE');
|
|
});
|
|
});
|
|
|
|
describe('PHP', () => {
|
|
it('parses class, function, and method declarations', async () => {
|
|
await loadLanguage(SupportedLanguages.PHP);
|
|
const content = readFixture('simple.php');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.PHP]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
const defTypes = defs.map(d => d.type);
|
|
expect(defTypes).toContain('definition.class');
|
|
});
|
|
});
|
|
|
|
describe('Swift', () => {
|
|
it('parses class, struct, protocol, and function if tree-sitter-swift is available', async () => {
|
|
try {
|
|
await loadLanguage(SupportedLanguages.Swift);
|
|
} catch {
|
|
// tree-sitter-swift not installed — skip
|
|
return;
|
|
}
|
|
|
|
const content = readFixture('simple.swift');
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[SupportedLanguages.Swift]);
|
|
const defs = extractDefinitions(matches);
|
|
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('gracefully handles missing tree-sitter-swift', async () => {
|
|
// If Swift is NOT available, loadLanguage should throw
|
|
// If it IS available, this test just passes
|
|
try {
|
|
await loadLanguage(SupportedLanguages.Swift);
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('Unsupported language');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('unhappy path', () => {
|
|
it('returns null/undefined for unsupported file extensions', () => {
|
|
expect(getLanguageFromFilename('archive.xyz')).toBeNull();
|
|
expect(getLanguageFromFilename('data.unknown')).toBeNull();
|
|
});
|
|
|
|
it('handles empty string file path', () => {
|
|
expect(getLanguageFromFilename('')).toBeNull();
|
|
});
|
|
|
|
it('returns null/undefined for binary file extensions', () => {
|
|
expect(getLanguageFromFilename('program.exe')).toBeNull();
|
|
expect(getLanguageFromFilename('library.dll')).toBeNull();
|
|
expect(getLanguageFromFilename('object.so')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('cross-language assertions', () => {
|
|
it('all supported languages produce at least one definition from fixtures', async () => {
|
|
const langFixtures: [SupportedLanguages, string, string?][] = [
|
|
[SupportedLanguages.TypeScript, 'simple.ts'],
|
|
[SupportedLanguages.JavaScript, 'simple.js'],
|
|
[SupportedLanguages.Python, 'simple.py'],
|
|
[SupportedLanguages.Java, 'simple.java'],
|
|
[SupportedLanguages.Go, 'simple.go'],
|
|
[SupportedLanguages.C, 'simple.c'],
|
|
[SupportedLanguages.CPlusPlus, 'simple.cpp'],
|
|
[SupportedLanguages.CSharp, 'simple.cs'],
|
|
[SupportedLanguages.Rust, 'simple.rs'],
|
|
[SupportedLanguages.PHP, 'simple.php'],
|
|
];
|
|
|
|
for (const [lang, fixture, filePath] of langFixtures) {
|
|
await loadLanguage(lang, filePath || fixture);
|
|
const content = readFixture(fixture);
|
|
const { matches } = parseAndQuery(parser, content, LANGUAGE_QUERIES[lang]);
|
|
const defs = extractDefinitions(matches);
|
|
expect(defs.length, `${lang} (${fixture}) should have definitions`).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('parser edge cases', () => {
|
|
it('loadLanguage throws for unsupported language', async () => {
|
|
await expect(loadLanguage('brainfuck' as any)).rejects.toThrow(/unsupported language/i);
|
|
});
|
|
|
|
it('parsing empty file content produces empty matches', async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'empty.ts');
|
|
const tree = parser.parse('');
|
|
expect(tree.rootNode).toBeDefined();
|
|
|
|
const lang = parser.getLanguage();
|
|
const query = new Parser.Query(lang, LANGUAGE_QUERIES[SupportedLanguages.TypeScript]);
|
|
const matches = query.matches(tree.rootNode);
|
|
expect(matches).toEqual([]);
|
|
});
|
|
|
|
it('parsing malformed code does not crash', async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'malformed.ts');
|
|
const tree = parser.parse('function {{{ class >>><< if(( end');
|
|
expect(tree.rootNode).toBeDefined();
|
|
expect(tree.rootNode.hasError).toBe(true);
|
|
});
|
|
});
|
|
});
|