-
-
Notifications
You must be signed in to change notification settings - Fork 51
test(corpus): pin what completes where the cursor is #998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+585
−4
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| //! The completion corpus: its format, a loader, and the runner. | ||
| //! | ||
| //! The argv corpus pins what a command line *binds*; `render` pins what a spec *reads as*. This | ||
| //! one pins what could go where the cursor is. | ||
| //! | ||
| //! # Why it exists | ||
| //! | ||
| //! Completion was the one area with three implementations and no shared fixture. | ||
| //! `argv/src/complete.rs` has its own unit tests, `cli/src/cli/complete_word.rs` has its own, and | ||
| //! a Go implementation landed with a third set. Three sets of tests written against three | ||
| //! readings of the same rules is the arrangement that produced every drift this project has had | ||
| //! to chase: the help renderers agreed on mise and differed on five of the other six jdx CLIs | ||
| //! until one fixture held them together. | ||
| //! | ||
| //! It also closes two thirds of PLAN.md's "not covered by the corpus yet" — completion parsing, | ||
| //! which is `parse_partial` over deliberately incomplete input, and restart tokens, which only | ||
| //! matter at a cursor. Mounts stay uncovered on purpose: resolving one *runs a command*, which a | ||
| //! corpus cannot do hermetically. | ||
| //! | ||
| //! # Every expectation here was measured first | ||
| //! | ||
| //! Two of the first vectors written asserted behaviour neither implementation has — a restart | ||
| //! token offering itself, and an attached `--format=j` completing its value. Both were plausible | ||
| //! and both were invented. The corpus is the definition of correct, so a vector may not assert a | ||
| //! rule on one author's opinion: what goes in is measured on both implementations, and where they | ||
| //! agree on something that looks wrong, the vector says so rather than pinning it silently. | ||
|
|
||
| use std::collections::BTreeSet; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use usage::Spec; | ||
| use usage_argv::complete::{complete, split, Shell}; | ||
|
|
||
| use crate::tables; | ||
|
|
||
| /// One `corpus/complete/*.json` file: a themed group of vectors. | ||
| #[derive(Debug, Deserialize, Serialize)] | ||
| #[serde(deny_unknown_fields)] | ||
| pub struct VectorFile { | ||
| /// What this file covers, e.g. `"positions"`. | ||
| pub section: String, | ||
| /// What the group establishes, and anything a reader needs in order to judge whether these | ||
| /// expectations are the right ones. | ||
| pub about: String, | ||
| pub vectors: Vec<Vector>, | ||
| } | ||
|
|
||
| /// A single case: ask `spec` what completes at the cursor in `line` and you must get `expect`. | ||
| #[derive(Debug, Deserialize, Serialize)] | ||
| #[serde(deny_unknown_fields)] | ||
| pub struct Vector { | ||
| /// Stable identifier, unique across the corpus. Failures quote it, so renaming one breaks | ||
| /// anybody tracking known failures. | ||
| pub id: String, | ||
| /// What this vector pins down, in one sentence. | ||
| pub doc: String, | ||
| /// A complete spec, as KDL. | ||
| pub spec: String, | ||
| /// The command line as typed, program name included. | ||
| pub line: String, | ||
| /// Where the cursor is, as a byte offset into `line`. Defaults to its end, which is where a | ||
| /// shell asks from; a vector completing mid-line says so. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub cursor: Option<usize>, | ||
| pub expect: Expect, | ||
| /// Whether `usage-cli`, the reference implementation, agrees with `expect`. | ||
| #[serde(default)] | ||
| pub reference: Reference, | ||
| } | ||
|
|
||
| impl Vector { | ||
| fn cursor(&self) -> usize { | ||
| self.cursor.unwrap_or(self.line.len()) | ||
| } | ||
| } | ||
|
|
||
| /// What completion must produce. | ||
| #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] | ||
| #[serde(deny_unknown_fields)] | ||
| pub struct Expect { | ||
| /// The words offered. Order-insensitive unless `ordered`: which order a shell shows them in | ||
| /// is the shell's business, and two implementations sorting differently is not a | ||
| /// disagreement about what completes. | ||
| #[serde(default)] | ||
| pub candidates: Vec<String>, | ||
| /// Whether the answer defers to the shell's own path completion. A vector cannot state what | ||
| /// the filesystem holds without becoming a test of the machine it runs on, so it states that | ||
| /// paths belong here and stops. | ||
| #[serde(default, skip_serializing_if = "std::ops::Not::not")] | ||
| pub files: bool, | ||
| /// Whether the order of `candidates` is itself the claim. | ||
| #[serde(default, skip_serializing_if = "std::ops::Not::not")] | ||
| pub ordered: bool, | ||
| } | ||
|
|
||
| /// Whether the reference implementation matches a vector's expectation. | ||
| #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum Reference { | ||
| /// `usage-cli` produces exactly `expect`. | ||
| #[default] | ||
| Agrees, | ||
| /// It produces something else. The note says what, and why the corpus keeps its own | ||
| /// expectation regardless. | ||
| Diverges(String), | ||
| } | ||
|
|
||
| /// What one implementation offered. | ||
| #[derive(Debug, PartialEq, Eq)] | ||
| pub struct Offered { | ||
| pub candidates: Vec<String>, | ||
| pub files: bool, | ||
| } | ||
|
|
||
| impl Offered { | ||
| /// Whether this satisfies a vector. Sorted unless the vector claims an order, so a failure | ||
| /// is about the set rather than about two implementations' sort stability. | ||
| pub fn matches(&self, expect: &Expect) -> bool { | ||
| if self.files != expect.files { | ||
| return false; | ||
| } | ||
| if expect.ordered { | ||
| return self.candidates == expect.candidates; | ||
| } | ||
| let mine: BTreeSet<&str> = self.candidates.iter().map(String::as_str).collect(); | ||
| let theirs: BTreeSet<&str> = expect.candidates.iter().map(String::as_str).collect(); | ||
| mine == theirs | ||
| } | ||
| } | ||
|
|
||
| /// What `usage-argv` offers for a vector. | ||
| /// | ||
| /// Its tables are built from the vector's KDL by `tables::build_spec`, which is the same bridge | ||
| /// the render corpus uses: the alternative is a Rust type per vector, and a corpus a reader | ||
| /// cannot extend is not one. | ||
| pub fn run(vector: &Vector) -> Result<Offered, String> { | ||
| let spec: Spec = vector | ||
| .spec | ||
| .parse() | ||
| .map_err(|e| format!("the spec would not load: {e}"))?; | ||
| let tables = tables::build_spec(&spec); | ||
| // Bash because the split has to be *some* shell and this one has no quoting rules of its | ||
| // own to fold in; which shell renders the answer is `complete::render`'s business, tested | ||
| // separately. | ||
| let at = split(&vector.line, vector.cursor(), Shell::Bash); | ||
| let answer = complete(tables, &at); | ||
| Ok(Offered { | ||
| candidates: answer.candidates.iter().map(|c| c.value.clone()).collect(), | ||
| files: answer.files.is_some(), | ||
| }) | ||
| } | ||
|
|
||
| /// What the reference `usage-cli` implementation offers for a vector. | ||
| pub fn reference(vector: &Vector) -> Result<Offered, String> { | ||
| let spec: Spec = vector | ||
| .spec | ||
| .parse() | ||
| .map_err(|e| format!("the spec would not load: {e}"))?; | ||
| let at = split(&vector.line, vector.cursor(), Shell::Bash); | ||
| let answer = usage_cli::complete_answer(&spec, &at.words, at.cword, "bash") | ||
| .map_err(|e| format!("the reference would not complete it: {e}"))?; | ||
| Ok(Offered { | ||
| candidates: if answer.files { | ||
| Vec::new() | ||
| } else { | ||
| answer | ||
| .candidates | ||
| .into_iter() | ||
| .map(|(value, _)| value) | ||
| .collect() | ||
| }, | ||
| files: answer.files, | ||
| }) | ||
| } | ||
|
|
||
| /// Every vector in the corpus, in file order. | ||
| pub fn load() -> Vec<(String, Vector)> { | ||
| let mut out = Vec::new(); | ||
| let mut files: Vec<PathBuf> = std::fs::read_dir(dir()) | ||
| .expect("the completion corpus should be readable") | ||
| .map(|entry| { | ||
| entry | ||
| .expect("a completion corpus directory entry should be readable") | ||
| .path() | ||
| }) | ||
| .filter(|p| p.extension().is_some_and(|e| e == "json")) | ||
| .collect(); | ||
| files.sort(); | ||
| for path in files { | ||
| let text = std::fs::read_to_string(&path).expect("a corpus file should be readable"); | ||
| let file: VectorFile = serde_json::from_str(&text) | ||
| .unwrap_or_else(|e| panic!("{} is not a valid corpus file: {e}", path.display())); | ||
| let name = path | ||
| .file_name() | ||
| .and_then(|n| n.to_str()) | ||
| .unwrap_or_default() | ||
| .to_string(); | ||
| for vector in file.vectors { | ||
| out.push((name.clone(), vector)); | ||
| } | ||
| } | ||
| out | ||
| } | ||
|
|
||
| fn dir() -> PathBuf { | ||
| Path::new(env!("CARGO_MANIFEST_DIR")).join("../corpus/complete") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.