1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
//! Real temporary Cargo repositories, and the production path from one to a
//! snapshot-bound resolution.
//!
//! Every fixture owns a `TempDir` and nothing else: no process, no socket, no
//! semantic database, no global cache, and no persistent file. Ordinary cases
//! release it by RAII on success, failure, and panic alike; the isolation case
//! closes it deliberately before it projects.
use std::fs;
use std::path::PathBuf;
use pedant_core::resolution::rust::{
CargoTargetKind, ResolutionLimits, RustProject, RustResolutionSnapshot, RustResolver,
RustTargetResolution, TargetId,
};
use pedant_graph::{CodeGraph, build_rust_graph};
use super::corpus::{BUILD_SCRIPT_CORPUS, GRAPH_CORPUS};
/// One temporary directory holding one materialized repository.
///
/// Owns the directory, every write into it, and the proof that it is gone.
/// Both language fixtures hold one: materializing a repository is the same job
/// whatever loader is going to read it, and so is releasing it.
pub struct TempRepository {
directory: tempfile::TempDir,
}
impl TempRepository {
/// Materialize `files` beneath a fresh temporary directory, in the order
/// the caller states them.
///
/// Directory enumeration is the filesystem's, not a loader's, so writing
/// one corpus in two orders is how a case perturbs what a walk sees without
/// changing what the repository says.
pub fn written<'a>(files: impl Iterator<Item = (&'a str, &'a str)>) -> Self {
let directory = tempfile::tempdir().expect("a temporary directory should be available");
let held = Self { directory };
for (relative, contents) in files {
held.write(relative, contents);
}
held
}
/// The repository root inside this directory.
pub fn root(&self) -> PathBuf {
self.directory.path().join("repo")
}
/// Write one file, creating every directory above it.
pub fn write(&self, relative: &str, contents: &str) {
let path = self.directory.path().join(relative);
let parent = path.parent().expect("a fixture path has a parent");
fs::create_dir_all(parent).unwrap_or_else(|error| panic!("{}: {error}", parent.display()));
fs::write(&path, contents.as_bytes())
.unwrap_or_else(|error| panic!("{}: {error}", path.display()));
}
/// Remove one stated file.
pub fn remove(&self, relative: &str) {
let path = self.directory.path().join(relative);
fs::remove_file(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display()));
}
/// Release the directory, prove it is gone, and answer with its root.
///
/// The proof travels with the release rather than being restated by each
/// caller: a case that reads what a fixture produced after closing it must
/// not be reading files that happen to still exist.
pub fn close(self) -> PathBuf {
let root = self.root();
self.directory
.close()
.expect("the fixture directory should close");
assert!(
!root.exists(),
"the fixture repository must be gone before what it produced is read"
);
root
}
}
/// One declared target: its owning package, its Cargo kind, and its name.
pub type DeclaredTarget = (&'static str, CargoTargetKind, &'static str);
/// The corpus library root every general case reads.
pub const CORPUS_LIBRARY: DeclaredTarget = ("app", CargoTargetKind::Library, "app");
/// The build-script root, the one target kind whose namespace exposes a build
/// dependency.
const BUILD_SCRIPT_ROOT: DeclaredTarget =
("app", CargoTargetKind::BuildScript, "build-script-build");
/// One materialized repository and the project loaded from it.
pub struct Fixture {
repository: TempRepository,
project: RustProject,
}
/// One snapshot and the syntactic resolution validated against it.
pub struct Resolved {
/// The completed snapshot.
pub snapshot: RustResolutionSnapshot,
/// The report bound to that snapshot.
pub resolution: RustTargetResolution,
}
impl Fixture {
/// Materialize `files` beneath a fresh temporary directory and load them.
pub fn build(files: &[(&str, &str)]) -> Self {
let repository = TempRepository::written(files.iter().copied());
let project = load(&repository, "fixture");
Self {
repository,
project,
}
}
/// The identity of one declared target.
pub fn target(&self, package: &str, kind: CargoTargetKind, name: &str) -> TargetId {
self.project
.targets()
.iter()
.find(|target| {
target.kind() == kind
&& target.name() == name
&& self
.project
.package(target.package())
.is_some_and(|owner| owner.name() == package)
})
.map(|target| target.id())
.unwrap_or_else(|| panic!("{package} declares no {kind:?} target named {name}"))
}
/// The identity of the one library target a single-package fixture holds.
pub fn sole_library(&self) -> TargetId {
let libraries: Vec<TargetId> = self
.project
.targets()
.iter()
.filter(|target| target.kind() == CargoTargetKind::Library)
.map(|target| target.id())
.collect();
match libraries.as_slice() {
[only] => *only,
found => panic!("the fixture declares {} library targets", found.len()),
}
}
/// Snapshot and syntactically resolve one target.
pub fn resolve(&self, target: TargetId) -> Resolved {
let snapshot = self
.project
.snapshot_resolution(target)
.unwrap_or_else(|error| panic!("the fixture target should snapshot: {error}"));
let resolution = RustResolver::resolve_syntactic(&snapshot)
.unwrap_or_else(|error| panic!("the fixture snapshot should resolve: {error}"));
Resolved {
snapshot,
resolution,
}
}
/// Snapshot and resolve the sole library target.
pub fn resolve_library(&self) -> Resolved {
self.resolve(self.sole_library())
}
/// Snapshot and resolve one declared target.
///
/// A repository stating more than one library names the one it means, so a
/// corpus that gains a dependency package keeps the root it is read at.
pub fn resolve_declared(&self, declared: DeclaredTarget) -> Resolved {
let (package, kind, name) = declared;
self.resolve(self.target(package, kind, name))
}
/// Rewrite one source in place and reload the project from the same root.
///
/// The manifests, the root path, and every target identity are unchanged,
/// so the reloaded project issues the same identities and only the snapshot
/// this fixture produces next differs.
pub fn rewrite(&mut self, relative: &str, contents: &str) {
self.revise(&[(relative, contents)], &[]);
}
/// Write every stated file, remove every stated path, and reload once.
///
/// One revision is one repository state: a rename is a written source, a
/// removed source, and the declaration that names it, and reloading between
/// those three would load a repository no case states.
pub fn revise(&mut self, written: &[(&str, &str)], removed: &[&str]) {
for (relative, contents) in written {
self.repository.write(relative, contents);
}
for relative in removed {
self.repository.remove(relative);
}
self.project = load(&self.repository, "revised fixture");
}
/// Release the repository, proving the directory is gone and answering with
/// the root a caller can name it by.
pub fn close(self) -> PathBuf {
drop(self.project);
self.repository.close()
}
}
/// The project one materialized repository loads under the default ceilings.
fn load(repository: &TempRepository, subject: &str) -> RustProject {
RustProject::load(&repository.root(), ResolutionLimits::default())
.unwrap_or_else(|error| panic!("the {subject} project should load: {error}"))
}
/// Materialize and resolve one corpus in one step.
pub fn resolve_library(files: &[(&str, &str)]) -> (Fixture, Resolved) {
let fixture = Fixture::build(files);
let resolved = fixture.resolve_library();
(fixture, resolved)
}
/// Materialize one corpus and resolve one target it declares.
///
/// The fixture travels with the resolution so the repository outlives every
/// case that reads it, exactly as [`resolve_library`] does.
pub fn resolve_target(files: &[(&str, &str)], declared: DeclaredTarget) -> (Fixture, Resolved) {
let fixture = Fixture::build(files);
let resolved = fixture.resolve_declared(declared);
(fixture, resolved)
}
/// Materialize one corpus, resolve one declared target, and project it.
pub fn project_target(
files: &[(&str, &str)],
declared: DeclaredTarget,
) -> (Fixture, Resolved, CodeGraph) {
let (fixture, resolved) = resolve_target(files, declared);
let graph = project(&resolved, &format!("{declared:?}"));
(fixture, resolved, graph)
}
/// The graph one resolution projects, named by the target that produced it.
fn project(resolved: &Resolved, subject: &str) -> CodeGraph {
build_rust_graph(&resolved.snapshot, &resolved.resolution)
.unwrap_or_else(|error| panic!("{subject} should project: {error}"))
}
/// The corpus library, resolved but not projected.
pub fn resolve_corpus_library() -> (Fixture, Resolved) {
resolve_target(GRAPH_CORPUS, CORPUS_LIBRARY)
}
/// The corpus library, resolved and projected.
pub fn project_corpus_library() -> (Fixture, Resolved, CodeGraph) {
project_target(GRAPH_CORPUS, CORPUS_LIBRARY)
}
/// The build-script root, resolved and projected.
pub fn project_build_script() -> (Fixture, Resolved, CodeGraph) {
project_target(BUILD_SCRIPT_CORPUS, BUILD_SCRIPT_ROOT)
}