Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/agents/e2e-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: e2e-test
description: Run end-to-end tests for dynwinrt code generation and WinRT API invocation
tools:
- powershell
- view
- edit
- create
---

# E2E Test Agent

You run and manage the dynwinrt end-to-end test suite.

## Commands

```powershell
# Run all E2E tests
.\tests\e2e_test.ps1 -SkipBuild

# Python only
.\tests\e2e_test.ps1 -SkipBuild -Lang py

# Full build + test
.\tests\e2e_test.ps1
```

## Adding test cases

Add entries to `tests/e2e_specs.json`. See `tests/e2e_specs.schema.json` for the field definitions.

Safe WinRT APIs to test (no extra dependencies):
- `Windows.Foundation`: Uri, PropertyValue, WwwFormUrlDecoder, MemoryBuffer
- `Windows.Globalization`: Calendar, Language, GeographicRegion
- `Windows.Devices.Geolocation`: Geopoint
- `Windows.Storage.Streams`: Buffer

Avoid APIs that need WinAppSDK, network, or user interaction.

## Diagnosing failures

1. Check `tests/e2e_generated/results_py.json` or `results_ts.json` for structured failure details
2. Inspect generated code in `tests/e2e_generated/py/` or `ts/`
3. Common issues: circular imports in codegen, naming mismatch (Python snake_case vs TS camelCase)
114 changes: 114 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copilot Agent Instructions

This file provides instructions for the GitHub Copilot coding agent when working on this repository.

## Project Overview

`dynwinrt` is a Rust library for dynamically invoking Windows Runtime (WinRT) APIs. It includes:
- **Core library** (`crates/dynwinrt/`) — Rust runtime using libffi for dynamic WinRT method invocation
- **JS binding** (`bindings/js/`) — napi-rs binding for Node.js/Electron
- **Python binding** (`bindings/py/`) — PyO3 binding for Python
- **Code generator** (`tools/winrt-meta/`) — Generates typed TypeScript and Python wrappers from .winmd metadata

## Build & Test Commands

```bash
# Build everything
cargo build

# Core library tests (52 tests, 1 ignored — requires WinAppSDK)
cargo test -p dynwinrt

# winrt-meta tests (49 unit tests + 1 snapshot test)
cargo test -p winrt-meta

# Python binding (requires Python 3.8+ and maturin)
cd bindings/py
python -m venv .venv && .venv/Scripts/Activate.ps1
pip install pytest maturin
maturin build
pip install target/wheels/*.whl --force-reinstall
python -m pytest tests/ -v

# JS binding (requires Node.js 18+)
cd bindings/js
npm install
npx napi build --no-const-enum --platform --release -o dist

# Code generation
cargo run -p winrt-meta -- generate --namespace Windows.Foundation --class-name Uri --lang ts --output ./generated
cargo run -p winrt-meta -- generate --namespace Windows.Foundation --class-name Uri --lang py --output ./generated

# E2E test (full pipeline: winmd → generate → call real WinRT APIs)
.\tests\e2e_test.ps1 -SkipBuild -Lang py
```

## E2E Testing

The E2E test framework validates the full pipeline: reading .winmd metadata → generating code → calling real Windows APIs.

### How it works

1. **Test specs** are defined in `tests/e2e_specs.json` (schema: `tests/e2e_specs.schema.json`) — each entry describes:
- `instantiate`: how to create an instance (`activate`, `static_factory`, or `none`)
- `checks`: array of assertions (`property_equals`, `property_exists`, `method_equals`, `method_result_contains`, `static_equals`, `static_not_null`)

2. **Runners** (`tests/runners/py_runner.py`, `tests/runners/ts_runner.ts`) read the specs and execute them, outputting `results.json`.

3. **Orchestrator** (`tests/e2e_test.ps1`) handles build, code generation, and runner invocation.

4. **Adding new test cases**: Add entries to `e2e_specs.json`:
```json
{
"namespace": "Windows.Foundation",
"class": "Uri",
"langs": ["py", "ts"],
"instantiate": { "kind": "static_factory", "method": "create_uri", "args": ["https://example.com"] },
"checks": [
{ "kind": "property_equals", "member": "host", "expected": "example.com" },
{ "kind": "method_result_contains", "member": "combine_uri", "args": ["sub/page"], "contains": "sub/page" },
{ "kind": "static_equals", "member": "escape_component", "args": ["hello world"], "expected": "hello%20world" }
]
}
```

### Testable WinRT APIs (no extra dependencies needed)

These APIs are available on any Windows 10/11 machine without WinAppSDK:
- `Windows.Foundation`: Uri, PropertyValue, WwwFormUrlDecoder, MemoryBuffer, Deferral
- `Windows.Data.Xml.Dom`: XmlDocument (has circular import issue in Python codegen)
- `Windows.Globalization`: Calendar, Language, GeographicRegion, CurrencyIdentifiers
- `Windows.Devices.Geolocation`: Geopoint (factory with struct parameter)
- `Windows.Storage.Streams`: Buffer
- `Windows.Security.Cryptography`: CryptographicBuffer

## Architecture Notes

### Type System Flow
```
.winmd metadata → MetadataTable (arena) → TypeHandle → MethodHandle → libffi call → WinRTValue
```

### Key Design Decisions
- **MetadataTable** is a global singleton (`LazyLock<Arc<MetadataTable>>`) shared across all bindings
- **Interface registration** is by IID (GUID) — each IID maps to one vtable with methods
- **Method invocation** returns a single `WinRTValue` (not a list) in Python binding
- **Generated code** uses relative imports (`from .module import Class`) — must be in a Python package

### Code Generator (winrt-meta)
- `src/codegen/typescript.rs` + `src/codegen/method.rs` — TypeScript generation
- `src/codegen/python.rs` + `src/codegen/py_method.rs` — Python generation
- `src/codegen/common.rs` — Shared helpers (type mapping, argument wrapping, return conversion)
- `--lang ts` generates `.ts` files with `DynWinRtType`/`DynWinRtValue` API (camelCase)
- `--lang py` generates `.py` files with `DynWinRTType`/`DynWinRTValue` API (snake_case)

### Python Binding API Names
- Types: `DynWinRTType.i32_type()`, `DynWinRTType.hstring()`, `DynWinRTType.bool_type()`, etc.
- Values: `DynWinRTValue.from_i32()`, `DynWinRTValue.from_hstring()`, `DynWinRTValue.from_bool()`
- GUID: `WinGUID.parse('...')`
- Method call: `method_handle.invoke(obj, [args])` → returns single `DynWinRTValue`

### Common Issues
- `test_initialize` is `#[ignore]` — requires `WINAPPSDK_BOOTSTRAP_DLL_PATH` env var
- Python `@property` must come before `@prop.setter` — codegen reorders methods for this
- Windows SDK winmd is at `C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd`
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,38 @@ jobs:
- name: Test winrt-meta
run: cargo test -p winrt-meta

# E2E tests: winmd → generate → call real WinRT APIs
e2e:
needs: test
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build winrt-meta
run: cargo build -p winrt-meta --release
- name: Build JS binding
working-directory: bindings/js
run: |
npm install
npx napi build --no-const-enum --platform --release -o dist
npm install --no-save tsx
- name: Build Python binding
run: |
cd bindings/py
python -m venv .venv
.venv/Scripts/Activate.ps1
pip install pytest maturin --quiet
maturin build --quiet
pip install (Get-ChildItem ../../target/wheels/*.whl | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName --force-reinstall --quiet
- name: Run E2E tests
run: .\tests\e2e_test.ps1 -SkipBuild

# winrt-meta (x64 + arm64)
winrt-meta:
needs: test
Expand Down
55 changes: 55 additions & 0 deletions .pipelines/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,58 @@ extends:
targetType: inline
workingDirectory: bindings/js
script: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist

# Install tsx for E2E TS runner
- task: PowerShell@2
displayName: Install tsx
inputs:
targetType: inline
workingDirectory: bindings/js
script: npm install --no-save tsx

# Build Python binding
- task: UsePythonVersion@0
displayName: Setup Python 3.12
inputs:
versionSpec: '3.12'

- task: PipAuthenticate@1
displayName: Authenticate Python feed
inputs:
artifactFeeds: 'pde-oss/dynwinrt-python'

- task: PowerShell@2
displayName: Build Python binding
inputs:
targetType: inline
script: |
cd bindings/py
python -m venv .venv
.venv/Scripts/Activate.ps1

# Configure pip to use Azure Artifacts feed
$pipIni = ".venv/pip.ini"
@"
[global]
index-url=https://pkgs.dev.azure.com/microsoft/pde-oss/_packaging/dynwinrt-python/pypi/simple/
"@ | Set-Content $pipIni

pip install pytest maturin --quiet
if ($LASTEXITCODE -ne 0) { Write-Error "pip install failed"; exit 1 }
maturin build --quiet
if ($LASTEXITCODE -ne 0) { Write-Error "maturin build failed"; exit 1 }
$whl = (Get-ChildItem ../../target/wheels/*.whl | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName
if (-not $whl) { Write-Error "No wheel found"; exit 1 }
pip install $whl --force-reinstall --quiet
if ($LASTEXITCODE -ne 0) { Write-Error "pip install failed"; exit 1 }

# E2E tests: winmd → generate → call real WinRT APIs
- task: PowerShell@2
displayName: Run E2E tests (40 tests)
inputs:
targetType: inline
script: |
cd bindings/py
if (Test-Path .venv) { .venv/Scripts/Activate.ps1 }
cd ../..
.\tests\e2e_test.ps1 -SkipBuild
Loading
Loading