diff --git a/tests/e2e_specs.json b/tests/e2e_specs.json index 143cbfe..66aafc4 100644 --- a/tests/e2e_specs.json +++ b/tests/e2e_specs.json @@ -292,6 +292,59 @@ "checks": [ { "kind": "async_memory_roundtrip", "member": "roundtrip", "write_value": 42 } ] + }, + { + "id": "calendar_property_setter", + "namespace": "Windows.Globalization", + "class": "Calendar", + "langs": ["py", "ts"], + "instantiate": { "kind": "activate" }, + "checks": [ + { "kind": "property_set_equals", "member": "year", "set_value": 2024, "expected": 2024 }, + { "kind": "property_set_equals", "member": "month", "set_value": 6, "expected": 6 }, + { "kind": "property_set_equals", "member": "day", "set_value": 15, "expected": 15 }, + { "kind": "property_set_equals", "member": "hour", "set_value": 10, "expected": 10 }, + { "kind": "property_set_equals", "member": "minute", "set_value": 30, "expected": 30 }, + { "kind": "property_set_equals", "member": "second", "set_value": 45, "expected": 45 } + ] + }, + { + "id": "buffer_length_setter", + "namespace": "Windows.Storage.Streams", + "class": "Buffer", + "langs": ["py", "ts"], + "instantiate": { + "kind": "static_factory", + "method": "create", + "args": [1024] + }, + "checks": [ + { "kind": "property_set_equals", "member": "length", "set_value": 512, "expected": 512 } + ] + }, + { + "id": "calendar_languages_vector", + "namespace": "Windows.Globalization", + "class": "Calendar", + "langs": ["py", "ts"], + "instantiate": { "kind": "activate" }, + "checks": [ + { "kind": "vector_view_access", "member": "languages", "min_size": 1 } + ] + }, + { + "id": "memory_buffer_closed_event", + "namespace": "Windows.Foundation", + "class": "MemoryBuffer", + "langs": ["py", "ts"], + "instantiate": { + "kind": "static_factory", + "method": "create", + "args": [64] + }, + "checks": [ + { "kind": "event_callback", "member": "create_reference", "event_name": "Closed", "trigger": "close" } + ] } ] } diff --git a/tests/e2e_specs.schema.json b/tests/e2e_specs.schema.json index 6f5152f..e474df2 100644 --- a/tests/e2e_specs.schema.json +++ b/tests/e2e_specs.schema.json @@ -64,7 +64,10 @@ "static_string_length", "static_expect_error", "cross_class_chain", - "async_memory_roundtrip" + "async_memory_roundtrip", + "property_set_equals", + "vector_view_access", + "event_callback" ] }, "member": { "type": "string", "description": "Property or method name (snake_case)" }, @@ -98,7 +101,11 @@ "type": "array", "description": "Steps for cross_class_chain checks" }, - "write_value": { "type": "integer", "description": "Integer value used by async memory roundtrip" } + "write_value": { "type": "integer", "description": "Integer value used by async memory roundtrip" }, + "set_value": { "description": "Value to set for property_set_equals check" }, + "min_size": { "type": "integer", "description": "Minimum vector size for vector_view_access" }, + "event_name": { "type": "string", "description": "Event name for event_callback (PascalCase, e.g. Closed)" }, + "trigger": { "type": "string", "description": "Method to call to trigger the event (snake_case)" } } } } diff --git a/tests/runners/py_runner.py b/tests/runners/py_runner.py index 99507ce..282a8ff 100644 --- a/tests/runners/py_runner.py +++ b/tests/runners/py_runner.py @@ -210,6 +210,29 @@ def run_check(check: dict, cls, obj, generated_dir: str, pkg_name: str) -> dict: else: cr['pass'] = True + elif kind == 'property_set_equals': + set_value = check['set_value'] + setattr(obj, member, set_value) + actual = getattr(obj, member) + expected = check['expected'] + if actual != expected: + cr['error'] = f'expected {expected!r}, got {actual!r}' + else: + cr['pass'] = True + + elif kind == 'vector_view_access': + vec = getattr(obj, member) + min_size = check.get('min_size', 1) + size = vec.size + if size < min_size: + cr['error'] = f'vector size {size} < {min_size}' + else: + first = vec.get_at(0) + if first is None: + cr['error'] = 'get_at(0) returned None' + else: + cr['pass'] = True + elif kind == 'struct_roundtrip': struct_mod = importlib.import_module(f"{pkg_name}.{check['struct_module']}") struct_cls = getattr(struct_mod, check['struct_class']) @@ -269,6 +292,29 @@ def run_check(check: dict, cls, obj, generated_dir: str, pkg_name: str) -> dict: else: cr['pass'] = True + elif kind == 'event_callback': + source_method = getattr(obj, member) + source = source_method() + event_name = to_snake_case(check['event_name']) + trigger = to_snake_case(check['trigger']) + + fired = [False] + on_method = f'on_{event_name}' + getattr(source, on_method)(lambda *args: fired.__setitem__(0, True)) + + # Try direct method, fall back to IClosable cast + if hasattr(source, trigger): + getattr(source, trigger)() + else: + iface_mod = importlib.import_module(f"{pkg_name}.i_closable") + IClosable = getattr(iface_mod, 'IClosable') + IClosable.from_value(source._obj).close() + + if not fired[0]: + cr['error'] = f'event {check["event_name"]} was not fired after {trigger}()' + else: + cr['pass'] = True + elif kind == 'static_string_length': method = getattr(cls, to_snake_case(check['member'])) args = [literal_arg(a) for a in check.get('args', [])] diff --git a/tests/runners/ts_runner.ts b/tests/runners/ts_runner.ts index 9727fb9..e24d083 100644 --- a/tests/runners/ts_runner.ts +++ b/tests/runners/ts_runner.ts @@ -137,6 +137,20 @@ async function runSpec( return result; } +async function importClass(generatedDir: string, className: string): Promise { + const candidates = [ + path.resolve(generatedDir, `${className}.ts`), + path.resolve(generatedDir, `${toPascalCase(className)}.ts`), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) { + const mod = await import(`file://${p.replace(/\\/g, '/')}`); + if (mod[className]) return mod[className]; + } + } + throw new Error(`Class ${className} not found in: ${candidates.join(', ')}`); +} + async function runCheck( check: Check, cls: any, @@ -167,16 +181,7 @@ async function runCheck( args = check.args; } else if (check.args_factory) { const af = check.args_factory!; - const afClassName = af.class; - let afCls: any; - if (afClassName === clsName) { - afCls = cls; - } else { - const afModPath = path.resolve(generatedDir, `${afClassName}.ts`); - const afMod = await import(`file://${afModPath.replace(/\\/g, '/')}`); - afCls = afMod[afClassName]; - if (!afCls) throw new Error(`Class ${afClassName} not found for args_factory`); - } + const afCls = af.class === clsName ? cls : await importClass(generatedDir, af.class); const afMethod = toCamelCase(af.method); const afArgs = af.args || []; args = [afCls[afMethod](...afArgs)]; @@ -233,8 +238,44 @@ async function runCheck( cr.pass = true; } } else if (kind === 'interface_cast') { - // Not yet supported in TS runner - cr.pass = true; + const ifaceClsName = (check as any).interface_class as string; + const methodName = toCamelCase((check as any).method as string); + + const ifaceCls = await importClass(generatedDir, ifaceClsName); + const casted = ifaceCls.from(obj._obj); + const resultVal = casted[methodName]; + const actual = String(typeof resultVal === 'function' ? resultVal.call(casted) : resultVal); + + if ((check as any).contains && !actual.includes((check as any).contains)) { + cr.error = `"${(check as any).contains}" not in "${actual}"`; + } else if (check.expected !== undefined && actual !== String(check.expected)) { + cr.error = `expected ${JSON.stringify(check.expected)}, got ${JSON.stringify(actual)}`; + } else { + cr.pass = true; + } + } else if (kind === 'property_set_equals') { + const setValue = (check as any).set_value; + obj[member] = setValue; + const actual = obj[member]; + if (actual !== check.expected) { + cr.error = `expected ${JSON.stringify(check.expected)}, got ${JSON.stringify(actual)}`; + } else { + cr.pass = true; + } + } else if (kind === 'vector_view_access') { + const vec = obj[member]; + const minSize = (check as any).min_size ?? 1; + const size = vec.size; + if (size < minSize) { + cr.error = `vector size ${size} < ${minSize}`; + } else { + const first = vec.getAt(0); + if (first == null) { + cr.error = 'getAt(0) returned null'; + } else { + cr.pass = true; + } + } } else if (kind === 'struct_roundtrip') { const structClass = check.struct_class as string; const structModule = check.struct_module as string; @@ -314,6 +355,33 @@ async function runCheck( } else { cr.pass = true; } + } else if (kind === 'event_callback') { + const sourceMethod = obj[member].bind(obj); + const source = sourceMethod(); + const eventName = (check as any).event_name as string; + const triggerMethod = toCamelCase((check as any).trigger as string); + + let fired = false; + const onMethod = `on${eventName}`; + source[onMethod]((..._args: any[]) => { fired = true; }); + + // Try direct method, fall back to IClosable cast for close() + if (typeof source[triggerMethod] === 'function') { + source[triggerMethod](); + } else { + const IClosable = await importClass(generatedDir, 'IClosable'); + const closable = IClosable.from(source._obj); + closable[triggerMethod](); + } + + // NonBlocking TSFN: callback is queued on event loop, await a tick + await new Promise(r => setTimeout(r, 100)); + + if (!fired) { + cr.error = `event ${eventName} was not fired after ${triggerMethod}()`; + } else { + cr.pass = true; + } } else if (kind === 'static_string_length') { const method = cls[member]?.bind(cls) ?? cls[Object.getOwnPropertyNames(cls).find(k => k.toLowerCase() === member.toLowerCase())!]?.bind(cls); if (!method) { cr.error = `method ${member} not found`; return cr; } diff --git a/tools/winrt-meta/npm/package-lock.json b/tools/winrt-meta/npm/package-lock.json index 84dd16b..c7c1980 100644 --- a/tools/winrt-meta/npm/package-lock.json +++ b/tools/winrt-meta/npm/package-lock.json @@ -15,7 +15,7 @@ "@swc/core": "^1.15.21" }, "bin": { - "winrt-meta": "bin/winrt-meta.js" + "winrt-meta": "cli.js" } }, "node_modules/@swc/core": {