Skip to content

Commit 82458a6

Browse files
committed
feat: add support for gvar test files
1 parent dafef91 commit 82458a6

11 files changed

Lines changed: 836 additions & 177 deletions

File tree

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,23 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
3030
- Tests only (with coverage): `make test` or `uv run pytest tests --cov=src`.
3131
- CLI smoke test without installing: `uv run python -m avrae_ls --analyze path/to/alias.txt`.
3232

33-
## Alias tests
33+
## Alias and gvar tests
3434

35-
- Create files ending with `.alias-test` (or `.aliastest`) next to your alias file. Each test starts with an invocation, followed by `---` and the expected result; you can stack multiple tests in one file by repeating this pattern (optional metadata after a second `---` per test).
35+
- `avrae-ls --run-tests [path]` discovers both alias tests and gvar tests and exits non-zero on failures.
36+
- Alias tests use `.alias-test` or `.aliastest` next to your alias file. Each test starts with an invocation, followed by `---` and the expected result; you can stack multiple tests in one file by repeating this pattern (optional metadata after a second `---` per test).
3637
```
3738
!my-alias -b example args
3839
---
3940
expected text or number
4041
```
42+
- Gvar tests use `.gvar-test` or `.gvartest` next to a sibling `.gvar` file with the same stem. The test body runs after an implicit `using(...)` import of that module under a sanitized local binding name.
43+
```
44+
return my_module.constant
45+
---
46+
expected value
47+
```
48+
- A gvar named `foo-bar.gvar` is exposed to tests as `foo_bar`; a leading digit becomes `gvar_<stem>`.
49+
- Multi-case `.gvar-test` files are supported. Separate cases with a second `---`, then a blank line before the next test body. Metadata after the second `---` is optional.
4150
- For embed aliases, put a YAML/JSON dictionary after the separator to compare against the embed preview (partial dictionaries are allowed).
4251
```
4352
!embedtest
@@ -57,7 +66,7 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
5766
name: Tester
5867
```
5968
`name` is a label for reporting, `vars` are merged into cvars/uvars/svars/gvars, and `character` keys are deep-merged into the mock character.
60-
- Run them with `avrae-ls --run-tests [path]` (defaults to the current directory); non-zero exit codes indicate failures.
69+
- Gvar tests compare the direct execution result of the test body, not alias preview/embed output.
6170

6271
## Config variable substitution
6372

action.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
name: "Avrae Alias Tests"
2-
description: "Run avrae-ls alias tests in CI."
1+
name: "Avrae Alias And Gvar Tests"
2+
description: "Run avrae-ls alias and gvar tests in CI."
33
inputs:
44
test-path:
5-
description: "Path to search for alias test files."
5+
description: "Path to search for alias and gvar test files."
66
required: false
77
default: "."
88
config-path:
@@ -39,7 +39,7 @@ runs:
3939
fi
4040
cp "${{ inputs.config-path }}" ".avraels.json"
4141
fi
42-
- name: Run alias tests
42+
- name: Run alias and gvar tests
4343
shell: bash
4444
env:
4545
AVRAE_TOKEN: ${{ inputs.avrae-token }}

docs/alias-tests.md

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Alias tests
1+
# Alias and gvar tests
22

3-
Alias tests let you run a mock alias and compare the result to what you expect. They are small text files that live next to your alias files and can contain many tests per file.
3+
Alias tests and gvar tests let you run mock Avrae code and compare the result to what you expect. They are small text files that live next to your alias or gvar files.
44

55
## Quick start
66

7-
1. Create an alias file (for example `greet.alias`).
8-
2. Create a test file next to it (for example `greet.alias-test`).
7+
1. Create an alias file such as `greet.alias`, or a gvar module such as `helpers.gvar`.
8+
2. Create a test file next to it such as `greet.alias-test` or `helpers.gvar-test`.
99
3. Run tests:
1010

1111
```bash
@@ -14,16 +14,19 @@ avrae-ls --run-tests
1414

1515
## File layout and discovery
1616

17-
Alias tests are discovered by filename. The runner scans for:
17+
Tests are discovered by filename. The runner scans for:
1818

1919
- `*.alias-test`
2020
- `*.aliastest`
21+
- `*.gvar-test`
22+
- `*.gvartest`
2123

2224
You can run tests in a folder or point to a single test file:
2325

2426
```bash
2527
avrae-ls --run-tests .
2628
avrae-ls --run-tests path/to/greet.alias-test
29+
avrae-ls --run-tests path/to/helpers.gvar-test
2730
```
2831

2932
Example layout:
@@ -33,6 +36,8 @@ my-aliases/
3336
.avraels.json
3437
greet.alias
3538
greet.alias-test
39+
helpers.gvar
40+
helpers.gvar-test
3641
combat/
3742
roll.alias
3843
roll.alias-test
@@ -47,6 +52,14 @@ The test file sits next to the alias file it targets. The test command decides w
4752

4853
If you keep aliases and tests side by side, you rarely need to think about this.
4954

55+
### How the gvar file is chosen
56+
57+
The gvar test file targets a sibling `.gvar` file with the same stem:
58+
59+
- `helpers.gvar-test` loads `helpers.gvar`
60+
- `foo-bar.gvar-test` loads `foo-bar.gvar`
61+
62+
5063
## Test file format
5164

5265
Each test has three sections:
@@ -170,17 +183,72 @@ If you just want to make sure the alias runs without error, leave the expected s
170183
name: "no-output-check"
171184
```
172185

186+
## Gvar test format
187+
188+
Each gvar test file contains draconic code that runs after the sibling gvar module is implicitly imported with `using(...)`.
189+
190+
```
191+
return helpers.constant
192+
---
193+
"Constant Gvar value"
194+
```
195+
196+
- The sibling `.gvar` stem becomes the imported gvar id.
197+
- The local binding uses the sanitized form of that stem.
198+
`foo-bar.gvar` becomes `foo_bar`
199+
`123mod.gvar` becomes `gvar_123mod`
200+
- Gvar tests compare the direct execution result of the test body.
201+
202+
### Multiple gvar tests per file
203+
204+
Use the same repeated block layout, but because there is no `!command` sentinel, include a second `---` before the blank line that separates cases.
205+
206+
```
207+
return helpers.constant
208+
---
209+
"one"
210+
---
211+
212+
return helpers.other
213+
---
214+
"two"
215+
```
216+
217+
- Metadata after the second `---` is optional.
218+
- Leave a blank line between cases.
219+
220+
### Gvar metadata
221+
222+
Gvar tests support the same metadata mapping as alias tests:
223+
224+
- `name`
225+
- `vars`
226+
- `character`
227+
228+
```
229+
return helpers.answer + get('bonus')
230+
---
231+
12
232+
---
233+
name: "adds-bonus"
234+
vars:
235+
cvars:
236+
bonus: 7
237+
```
238+
173239
## Common tips
174240

175241
- Expected output cannot include a line that starts with `!` because that marks the next test. If you need to check a `!` line, use a single-line string like `"line1\n!line2"` or a regex like `re:^!`.
176242
- If you want a number treated as text, wrap it in quotes (YAML reads bare numbers as numbers).
177243
- The mock context comes from `.avraels.json` and any var files you configure there (including gvar `{ filePath: ... }` entries). Metadata `vars` and `character` values are merged on top of those defaults for the test only.
178244
- Each test runs independently, so one test does not affect another.
179-
- The expected section is compared against the alias result or embed preview, not against stdout. Stdout is shown in the test report to help debug.
245+
- Alias tests compare against the alias result or embed preview, not stdout.
246+
- Gvar tests compare against the direct test-body result, not stdout.
247+
- Stdout is still shown in the test report to help debug.
180248

181249
## GitHub Actions
182250

183-
This repo ships a composite action that installs `avrae-ls` and runs alias tests.
251+
This repo ships a composite action that installs `avrae-ls` and runs alias and gvar tests.
184252

185253
1. Create a minimal config file for CI (for example `.github/avrae/ci.avraels.json`):
186254

@@ -196,7 +264,7 @@ This repo ships a composite action that installs `avrae-ls` and runs alias tests
196264
2. Add a workflow that uses the action:
197265

198266
```yaml
199-
name: Alias Tests
267+
name: Alias And Gvar Tests
200268

201269
on:
202270
pull_request:
@@ -224,22 +292,26 @@ Use the file path:
224292

225293
```bash
226294
avrae-ls --run-tests path/to/greet.alias-test
295+
avrae-ls --run-tests path/to/helpers.gvar-test
227296
```
228297

229298
**Can I put many tests in one file?**
230-
Yes. Repeat the `command`, `---`, and `expected` blocks as many times as you want.
299+
Yes. Alias tests can repeat the `command`, `---`, and `expected` blocks. Gvar tests can repeat `body`, `---`, and `expected`, but multi-case gvar files should include the second `---` and a blank line between cases.
231300

232301
**What if I do not care about the output, only that it runs?**
233302
Leave the expected section empty (see "Run-only tests"). That will pass as long as the alias runs without errors.
234303

235304
**How does it find the alias file?**
236305
It looks in the same folder as the test file for the alias name in your command (for example `!greet` matches `greet`, `greet.alias`, or `greet.txt`). If your test file is named `test-greet.alias-test`, it will also try `greet`.
237306

307+
**How does it find the gvar file?**
308+
It loads the sibling `.gvar` file with the same stem as the `.gvar-test` file.
309+
238310
**Can I use JSON instead of YAML?**
239311
Yes. JSON is valid YAML, so JSON objects and lists work in the expected and metadata sections.
240312

241313
**Why is a gvar missing or `using(...)` failing?**
242-
Make sure `.avraels.json` enables gvar fetch and sets a token, or provide the gvar in your var files (inline or with `{ filePath: ... }`) or test metadata under `vars.gvars`.
314+
Make sure `.avraels.json` enables gvar fetch and sets a token, or provide the dependency gvar in your var files (inline or with `{ filePath: ... }`) or test metadata under `vars.gvars`. The module under test itself comes from the sibling `.gvar` file.
243315

244316
**How do I ignore gvar fetch failures in CLI runs?**
245317
Use `avrae-ls --run-tests --silent-gvar-fetch` to treat remote gvar fetch failures as `None` without warnings.

src/avrae_ls/__main__.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
parse_alias_tests,
2121
run_alias_tests,
2222
)
23+
from avrae_ls.testing.gvar_tests import GVarTestError, GVarTestResult, parse_gvar_tests, run_gvar_tests
2324
from avrae_ls.config import AvraeLSConfig, CONFIG_FILENAME, load_config
2425
from avrae_ls.runtime.context import ContextBuilder
2526
from avrae_ls.analysis.diagnostics import DiagnosticProvider
2627
from avrae_ls.runtime.runtime import MockExecutor
2728
from avrae_ls.lsp.server import create_server, __version__
2829

30+
ALIAS_TEST_PATTERNS = ("*.alias-test", "*.aliastest")
31+
GVAR_TEST_PATTERNS = ("*.gvar-test", "*.gvartest")
32+
RUN_TEST_PATTERNS = ALIAS_TEST_PATTERNS + GVAR_TEST_PATTERNS
33+
2934

3035
def main(argv: list[str] | None = None) -> None:
3136
parser = argparse.ArgumentParser(description="Avrae draconic alias language server")
@@ -40,7 +45,7 @@ def main(argv: list[str] | None = None) -> None:
4045
metavar="PATH",
4146
nargs="?",
4247
const=".",
43-
help="Run alias tests in PATH (defaults to current directory)",
48+
help="Run alias and gvar tests in PATH (defaults to current directory)",
4449
)
4550
parser.add_argument(
4651
"--silent-gvar-fetch",
@@ -148,7 +153,7 @@ def _run_alias_tests(
148153

149154
workspace_root = _discover_workspace_root(target)
150155
log = logging.getLogger(__name__)
151-
log.info("Running alias tests in %s (workspace root: %s)", target, workspace_root)
156+
log.info("Running alias and gvar tests in %s (workspace root: %s)", target, workspace_root)
152157

153158
config = _load_runtime_config(
154159
workspace_root,
@@ -161,23 +166,29 @@ def _run_alias_tests(
161166
builder = ContextBuilder(config)
162167
executor = MockExecutor(config.service)
163168

164-
test_files = discover_test_files(target)
165-
cases = []
169+
test_files = discover_test_files(target, patterns=RUN_TEST_PATTERNS)
170+
alias_cases = []
171+
gvar_cases = []
166172
parse_errors: list[str] = []
167173
for test_file in test_files:
168174
try:
169-
cases.extend(parse_alias_tests(test_file))
170-
except AliasTestError as exc:
175+
if _is_gvar_test_file(test_file):
176+
gvar_cases.extend(parse_gvar_tests(test_file))
177+
else:
178+
alias_cases.extend(parse_alias_tests(test_file))
179+
except (AliasTestError, GVarTestError) as exc:
171180
parse_errors.append(str(exc))
172181

173182
if parse_errors:
174183
for err in parse_errors:
175184
print(err, file=sys.stderr)
176-
if not cases:
177-
print(f"No alias tests found under {target}")
185+
if not alias_cases and not gvar_cases:
186+
print(f"No alias or gvar tests found under {target}")
178187
return 1 if parse_errors else 0
179188

180-
results = asyncio.run(run_alias_tests(cases, builder, executor))
189+
alias_results = asyncio.run(run_alias_tests(alias_cases, builder, executor)) if alias_cases else []
190+
gvar_results = asyncio.run(run_gvar_tests(gvar_cases, builder, executor)) if gvar_cases else []
191+
results = [*alias_results, *gvar_results]
181192
_print_test_results(results, workspace_root)
182193

183194
failures = [res for res in results if not res.passed]
@@ -204,14 +215,14 @@ def _load_runtime_config(
204215
return config
205216

206217

207-
def _print_test_results(results: Sequence[AliasTestResult], workspace_root: Path) -> None:
218+
def _print_test_results(results: Sequence[AliasTestResult | GVarTestResult], workspace_root: Path) -> None:
208219
total = len(results)
209220
passed = 0
210221
for res in results:
211222
rel = _relative_to_workspace(res.case.path, workspace_root)
212223
label = f"{rel} ({res.case.name})" if res.case.name else rel
213224
status = "PASS" if res.passed else "FAIL"
214-
print(f"[{status}] {label} (alias: {res.case.alias_name})")
225+
print(f"[{status}] {label} ({res.case.target_kind}: {res.case.target_name})")
215226
if res.passed:
216227
if res.stdout:
217228
print(f" Stdout: {res.stdout.strip()}")
@@ -323,6 +334,10 @@ def _discover_workspace_root(target: Path) -> Path:
323334
return current
324335

325336

337+
def _is_gvar_test_file(path: Path) -> bool:
338+
return path.name.endswith(".gvar-test") or path.name.endswith(".gvartest")
339+
340+
326341
def _print_diagnostics(path: Path, diagnostics: Iterable[types.Diagnostic]) -> None:
327342
diags = list(diagnostics)
328343
if not diags:

src/avrae_ls/gvar_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
3+
import re
4+
5+
6+
def sanitize_gvar_binding(label: str) -> str:
7+
cleaned = re.sub(r"\W+", "_", str(label))
8+
if cleaned and cleaned[0].isdigit():
9+
cleaned = f"gvar_{cleaned}"
10+
return cleaned or "gvar_import"

src/avrae_ls/lsp/code_actions.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from lsprotocol import types
1111

12+
from avrae_ls.gvar_utils import sanitize_gvar_binding
1213
from avrae_ls.lsp.codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
1314
from avrae_ls.analysis.parser import DraconicBlock
1415
from avrae_ls.analysis.source_context import build_source_context
@@ -130,7 +131,7 @@ def _using_stub_action(
130131
diag: types.Diagnostic,
131132
gvar_id: str,
132133
) -> types.CodeAction:
133-
alias = _sanitize_symbol(gvar_id)
134+
alias = sanitize_gvar_binding(gvar_id)
134135
insertion_line, indent = _block_insertion(blocks, diag.range.start.line, source)
135136
edit = _insert_line_edit(insertion_line, indent, f'using({alias}="{gvar_id}")\n')
136137
return types.CodeAction(
@@ -184,15 +185,6 @@ def _line_indent(source: str, line: int, default: int = 0) -> int:
184185
if match:
185186
return len(match.group(1))
186187
return default
187-
188-
189-
def _sanitize_symbol(label: str) -> str:
190-
cleaned = re.sub(r"\W+", "_", str(label))
191-
if cleaned and cleaned[0].isdigit():
192-
cleaned = f"gvar_{cleaned}"
193-
return cleaned or "gvar_import"
194-
195-
196188
def _kind_allowed(kind: str, only: Sequence[str]) -> bool:
197189
if not only:
198190
return True

src/avrae_ls/testing/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
"""Alias test file parsing and execution helpers."""
1+
"""Alias and gvar test file parsing and execution helpers."""
22

33
from . import alias_tests
4+
from . import gvar_tests
45

5-
__all__ = ["alias_tests"]
6+
__all__ = ["alias_tests", "gvar_tests"]

0 commit comments

Comments
 (0)