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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ its internal AST.

[`opy-rs`](https://github.com/wrightkit/opy-rs) ships `opy-provider`, the
LPP provider Wright uses for OverPy. The spec covers protocol versions 1.0
to 1.6.
to 1.7.

## Implementing a provider

Expand Down
4 changes: 2 additions & 2 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LPP v1 Conformance Suite

This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry, LPP 1.2 directory-target project-loading, LPP 1.3 source-identity, LPP 1.4 artifact-format negotiation, LPP 1.5 name-lookup, and LPP 1.6 canonical-id-resolution revisions (see [`../docs/spec/README.md`](../docs/spec/README.md)).
This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry, LPP 1.2 directory-target project-loading, LPP 1.3 source-identity, LPP 1.4 artifact-format negotiation, LPP 1.5 name-lookup, LPP 1.6 canonical-id-resolution, and LPP 1.7 compile-inputs revisions (see [`../docs/spec/README.md`](../docs/spec/README.md)).

## Layout

Expand All @@ -12,7 +12,7 @@ mock-provider/ Reference provider for the "x-demo-lang" equation DSL (Ru
runner/ Conformance runner that replays fixtures against any provider binary
```

* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, LPP 1.2, LPP 1.3, LPP 1.4, LPP 1.5, and LPP 1.6 scenarios. `fixtures/v1/sessions/` holds the standard initialize/shutdown handshake once per protocol version; a scenario whose steps don't themselves exercise the handshake sets `"session": "<version>"` and the runner splices those steps around its own.
* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, LPP 1.2, LPP 1.3, LPP 1.4, LPP 1.5, LPP 1.6, and LPP 1.7 scenarios. `fixtures/v1/sessions/` holds the standard initialize/shutdown handshake once per protocol version; a scenario whose steps don't themselves exercise the handshake sets `"session": "<version>"` and the runner splices those steps around its own.
* **Mock provider** (`mock-provider/`): a small Rust binary implementing the full LPP v1 surface for a demonstration language distinct from OPY and OSTW. It runs over stdio so clients (like the Wright LPP client in wrightkit/wright#142) can test against it directly.
* **Runner** (`runner/`): spawns a fresh provider process per scenario, feeds requests over stdin, validates stdout responses against expectations, and checks the process exit code. Expected responses compare verbatim except at leaves a fixture marks provider-supplied (see the scenario format below).

Expand Down
15 changes: 15 additions & 0 deletions conformance/fixtures/v1/69-compile-inputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "compile-inputs",
"description": "LPP 1.7 advertises compile inputs and returns every path the entry-based compile accessed. The project directory is listed because the compile listed its entries; notes.txt is not listed because its content is never read, so editing it cannot change the result.",
"scope": "semantics",
"session": "1.7",
"providerArgs": ["--protocol-version", "1.7"],
"projectFiles": {
"entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n",
"notes.txt": "not a source file\n",
"support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n"
},
"steps": [
{"request":{"id":2,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"compileInputs":["${PROJECT_URI}","${PROJECT_URI}/entry.xdl","${PROJECT_URI}/support.xdl"],"sourceIdentity":"a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}
]
}
15 changes: 15 additions & 0 deletions conformance/fixtures/v1/70-compile-inputs-unavailable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "compile-inputs-unavailable",
"description": "An LPP 1.7 provider may decline compile inputs independently; entry compilation remains available and the result omits the field.",
"scope": "protocol",
"providerArgs": ["--protocol-version", "1.7", "--without", "compileInputs"],
"projectFiles": {
"entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n",
"support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n"
},
"steps": [
{"request":{"id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.7"}},"expectResponse":{"result":{"protocolVersion":"1.7","serverInfo":{"name":{"$provider":"nonEmptyString"},"version":{"$provider":"nonEmptyString"}},"languages":{"$provider":"languageList"},"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true,"sourceIdentity":true,"lookup":true,"resolveIds":true,"compileInputs":false}}}},
{"request":{"id":2,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}},
{"request":{"id":3,"method":"lpp/shutdown","params":{}},"expectResponse":{"result":null}}
]
}
31 changes: 31 additions & 0 deletions conformance/fixtures/v1/sessions/1.7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"initialize": {
"request": { "id": "init", "method": "lpp/initialize", "params": { "protocolVersion": "1.7" } },
"expectResponse": {
"result": {
"protocolVersion": "1.7",
"serverInfo": { "name": { "$provider": "nonEmptyString" }, "version": { "$provider": "nonEmptyString" } },
"languages": { "$provider": "languageList" },
"capabilities": {
"check": { "$provider": "boolean" },
"compile": { "$provider": "boolean" },
"reconstruct": { "$provider": "boolean" },
"symbols": { "$provider": "boolean" },
"definition": { "$provider": "boolean" },
"references": { "$provider": "boolean" },
"rename": { "$provider": "boolean" },
"editValidation": { "$provider": "boolean" },
"projectLoading": { "$provider": "boolean" },
"sourceIdentity": { "$provider": "boolean" },
"lookup": { "$provider": "boolean" },
"resolveIds": { "$provider": "boolean" },
"compileInputs": { "$provider": "boolean" }
}
}
}
},
"shutdown": {
"request": { "id": "shutdown", "method": "lpp/shutdown", "params": {} },
"expectResponse": { "result": null }
}
}
17 changes: 10 additions & 7 deletions conformance/mock-provider/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ impl Server {

fn compile(&self, params: Value) -> Result<Value, HandlerError> {
let accepted = accepted_artifact_formats(self, &params)?;
let (documents, entry_uri) =
let (documents, loaded) =
documents_for_request(self, parse_params(params)?, "lpp/compile")?;
if entry_uri.is_none() && documents.len() != 1 {
if loaded.is_none() && documents.len() != 1 {
return Err(HandlerError::refusal(
"compile.requiresSingleDocument",
json!({}),
"compile requires exactly one document",
));
}
let entry = match &entry_uri {
Some(uri) => &documents[uri],
let entry = match &loaded {
Some(loaded) => &documents[&loaded.uri],
None => documents.values().next().expect("len == 1"),
};
check_document(entry)?;
Expand Down Expand Up @@ -127,9 +127,12 @@ impl Server {
json!({ "format": format, "content": content })
};
let mut result = json!({ "diagnostics": diagnostics, "artifact": artifact });
if self.capability("sourceIdentity") {
if let Some(uri) = &entry_uri {
result["sourceIdentity"] = json!(sha256_hex(&documents[uri].text));
if let Some(loaded) = &loaded {
if self.capability("sourceIdentity") {
result["sourceIdentity"] = json!(sha256_hex(&entry.text));
}
if self.capability("compileInputs") {
result["compileInputs"] = json!(loaded.inputs);
}
}
Ok(result)
Expand Down
74 changes: 67 additions & 7 deletions conformance/mock-provider/src/project.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::collections::{BTreeSet, HashMap};
use std::path::{Component, Path, PathBuf};

use lpp_conformance_common::{file_uri_to_path, path_to_file_uri};
use serde_json::json;
Expand All @@ -8,11 +8,16 @@ use crate::rpc::HandlerError;
use crate::server::{LANGUAGE_ID, Server};
use crate::wire::{DocsParams, Document, ProjectEntry};

pub(crate) struct LoadedEntry {
pub(crate) uri: String,
pub(crate) inputs: Vec<String>,
}

pub(crate) fn documents_for_request(
server: &Server,
params: DocsParams,
method: &str,
) -> Result<(HashMap<String, Document>, Option<String>), HandlerError> {
) -> Result<(HashMap<String, Document>, Option<LoadedEntry>), HandlerError> {
match (params.documents, params.entry) {
(Some(documents), None) => Ok((documents, None)),
(None, Some(entry)) => {
Expand All @@ -26,12 +31,12 @@ pub(crate) fn documents_for_request(
"capability 'projectLoading' is not available",
));
}
let (documents, entry_uri) = Loader {
let (documents, loaded) = Loader {
server,
entry: &entry,
}
.run()?;
Ok((documents, Some(entry_uri)))
Ok((documents, Some(loaded)))
}
_ => Err(HandlerError::invalid_params()),
}
Expand Down Expand Up @@ -65,7 +70,7 @@ impl Loader<'_> {
HandlerError::lpp("projectLoadFailed", details, message)
}

fn run(self) -> Result<(HashMap<String, Document>, String), HandlerError> {
fn run(self) -> Result<(HashMap<String, Document>, LoadedEntry), HandlerError> {
let entry = self.entry;
if entry.version < 0 {
return Err(self.invalid(
Expand Down Expand Up @@ -102,6 +107,7 @@ impl Loader<'_> {
));
};

let mut traversed = vec![path.clone()];
let path = std::fs::canonicalize(&path).map_err(|_| {
let (reason, message) = match target {
Target::File => ("entryNotFound", "project entry could not be loaded"),
Expand Down Expand Up @@ -133,6 +139,7 @@ impl Loader<'_> {
));
}
let selected = path.join("entry.xdl");
traversed.push(selected.clone());
if !selected.is_file() {
return Err(self.fail(
"defaultEntryNotFound",
Expand Down Expand Up @@ -200,6 +207,8 @@ impl Loader<'_> {
}
paths.sort();

traversed.push(root.clone());
traversed.extend(paths.iter().cloned());
let mut documents = HashMap::new();
for path in paths {
let unresolved_uri = path_to_file_uri(&path);
Expand Down Expand Up @@ -234,6 +243,57 @@ impl Loader<'_> {
},
);
}
Ok((documents, canonical_entry_uri))
let inputs = documents
.keys()
.cloned()
.chain(declared_paths(&traversed).filter_map(|path| path_to_file_uri(&path)))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
Ok((
documents,
LoadedEntry {
uri: canonical_entry_uri,
inputs,
},
))
}
}

/// Each traversed path plus every symbolic link followed while resolving it,
/// including links in a chain and linked directory components, so a
/// retargeted link shows up as a changed declared input.
fn declared_paths(traversed: &[PathBuf]) -> impl Iterator<Item = PathBuf> + '_ {
traversed.iter().flat_map(|path| {
let mut declared = vec![path.clone()];
let mut hops = 0;
resolve(path, &mut declared, &mut hops);
declared
})
}

const MAX_LINK_HOPS: u32 = 40;

fn resolve(path: &Path, links: &mut Vec<PathBuf>, hops: &mut u32) -> PathBuf {
let mut resolved = PathBuf::from("/");
for component in path.components() {
match component {
Component::Normal(name) => {
let next = resolved.join(name);
match std::fs::read_link(&next) {
Ok(target) if *hops < MAX_LINK_HOPS => {
*hops += 1;
links.push(next);
resolved = resolve(&resolved.join(target), links, hops);
}
_ => resolved = next,
}
}
Component::ParentDir => {
resolved.pop();
}
Component::RootDir | Component::CurDir | Component::Prefix(_) => {}
}
}
resolved
}
7 changes: 6 additions & 1 deletion conformance/mock-provider/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::methods::METHODS;
use crate::rpc::{lpp_error, ok, parse_params, std_error};
use crate::wire::InitParams;

pub(crate) const PROTOCOL_VERSIONS: [&str; 7] = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"];
pub(crate) const PROTOCOL_VERSIONS: [&str; 8] =
["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7"];
pub(crate) const DEFAULT_PROTOCOL_VERSION: &str = PROTOCOL_VERSIONS[0];
pub(crate) const LANGUAGE_ID: &str = "x-demo-lang";

Expand Down Expand Up @@ -67,6 +68,10 @@ const CAPABILITIES: &[Capability] = &[
name: "resolveIds",
min_minor: 6,
},
Capability {
name: "compileInputs",
min_minor: 7,
},
];

fn minor_of(version: &str) -> Option<u32> {
Expand Down
70 changes: 70 additions & 0 deletions conformance/mock-provider/tests/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,73 @@ fn project_load_failure_names_the_broken_source_uri() {
let uri = lpp["details"]["uri"].as_str().unwrap_or_default();
assert!(uri.ends_with("/broken.xdl"), "details.uri: {uri}");
}

#[cfg(unix)]
#[test]
fn compile_inputs_declare_followed_symlinks() {
let base = project_dir("compile-inputs-symlink");
let root = base.join("proj");
std::fs::create_dir_all(base.join("lib")).unwrap();
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("entry.xdl"), VALID).unwrap();
std::fs::write(base.join("lib/a.xdl"), VALID).unwrap();
std::os::unix::fs::symlink("../lib/a.xdl", root.join("support.xdl")).unwrap();
let root = root.canonicalize().unwrap();
let init =
r#"{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.7"}}"#;
let request = format!(
r#"{{"jsonrpc":"2.0","id":2,"method":"lpp/compile","params":{{"entry":{{"uri":"file://{}","languageId":"x-demo-lang","version":1,"kind":"directory"}}}}}}"#,
root.display()
);
let responses = run_session_args(
&["--protocol-version", "1.7"],
format!("{init}\n{request}\n").as_bytes(),
);
std::fs::remove_dir_all(&base).ok();
let inputs = &responses[1]["result"]["compileInputs"];
let link = format!("file://{}/support.xdl", root.display());
let target = format!("file://{}/lib/a.xdl", root.parent().unwrap().display());
for uri in [&link, &target] {
assert!(
inputs.as_array().unwrap().contains(&serde_json::json!(uri)),
"{uri} missing from {inputs}"
);
}
}

#[cfg(unix)]
#[test]
fn compile_inputs_declare_links_followed_during_resolution() {
let base = project_dir("compile-inputs-link-chain");
let root = base.join("proj");
std::fs::create_dir_all(base.join("lib")).unwrap();
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("entry.xdl"), VALID).unwrap();
std::fs::write(base.join("lib/a.xdl"), VALID).unwrap();
std::fs::write(base.join("lib/c.xdl"), VALID).unwrap();
std::os::unix::fs::symlink("a.xdl", base.join("lib/mid.xdl")).unwrap();
std::os::unix::fs::symlink("../lib/mid.xdl", root.join("chain.xdl")).unwrap();
std::os::unix::fs::symlink("lib", base.join("ld")).unwrap();
std::os::unix::fs::symlink("../ld/c.xdl", root.join("linked-dir.xdl")).unwrap();
let base = base.canonicalize().unwrap();
let root = base.join("proj");
let init =
r#"{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.7"}}"#;
let request = format!(
r#"{{"jsonrpc":"2.0","id":2,"method":"lpp/compile","params":{{"entry":{{"uri":"file://{}","languageId":"x-demo-lang","version":1,"kind":"directory"}}}}}}"#,
root.display()
);
let responses = run_session_args(
&["--protocol-version", "1.7"],
format!("{init}\n{request}\n").as_bytes(),
);
std::fs::remove_dir_all(&base).ok();
let inputs = &responses[1]["result"]["compileInputs"];
for path in ["proj/chain.xdl", "lib/mid.xdl", "proj/linked-dir.xdl", "ld"] {
let uri = format!("file://{}/{path}", base.display());
assert!(
inputs.as_array().unwrap().contains(&serde_json::json!(uri)),
"{uri} missing from {inputs}"
);
}
}
Loading
Loading