Skip to content

Commit 7261ede

Browse files
authored
Refactor build backend testing (#13464)
This is useful for adding more tests for build backend configuration.
1 parent 4ea44be commit 7261ede

File tree

1 file changed

+156
-113
lines changed
  • crates/uv-build-backend/src

1 file changed

+156
-113
lines changed

crates/uv-build-backend/src/lib.rs

Lines changed: 156 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,124 @@ mod tests {
281281
use sha2::Digest;
282282
use std::io::{BufReader, Read};
283283
use tempfile::TempDir;
284+
use uv_distribution_filename::{SourceDistFilename, WheelFilename};
284285
use uv_fs::{copy_dir_all, relative_to};
285286

287+
/// File listings, generated archives and archive contents for both a build with
288+
/// source tree -> wheel
289+
/// and a build with
290+
/// source tree -> source dist -> wheel.
291+
struct BuildResults {
292+
source_dist_list_files: FileList,
293+
source_dist_filename: SourceDistFilename,
294+
source_dist_contents: Vec<String>,
295+
wheel_list_files: FileList,
296+
wheel_filename: WheelFilename,
297+
wheel_contents: Vec<String>,
298+
}
299+
300+
/// Run both a direct wheel build and an indirect wheel build through a source distribution,
301+
/// while checking that directly built wheel and indirectly built wheel are the same.
302+
fn build(source_root: &Path, dist: &Path) -> BuildResults {
303+
// Build a direct wheel, capture all its properties to compare it with the indirect wheel
304+
// latest and remove it since it has the same filename as the indirect wheel.
305+
let (_name, direct_wheel_list_files) = list_wheel(source_root, "1.0.0+test").unwrap();
306+
let direct_wheel_filename = build_wheel(source_root, dist, None, "1.0.0+test").unwrap();
307+
let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
308+
let direct_wheel_contents = wheel_contents(&direct_wheel_path);
309+
let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path).unwrap());
310+
fs_err::remove_file(&direct_wheel_path).unwrap();
311+
312+
// Build a source distribution.
313+
let (_name, source_dist_list_files) = list_source_dist(source_root, "1.0.0+test").unwrap();
314+
// TODO(konsti): This should run in the unpacked source dist tempdir, but we need to
315+
// normalize the path.
316+
let (_name, wheel_list_files) = list_wheel(source_root, "1.0.0+test").unwrap();
317+
let source_dist_filename = build_source_dist(source_root, dist, "1.0.0+test").unwrap();
318+
let source_dist_path = dist.join(source_dist_filename.to_string());
319+
let source_dist_contents = sdist_contents(&source_dist_path);
320+
321+
// Unpack the source distribution and build a wheel from it.
322+
let sdist_tree = TempDir::new().unwrap();
323+
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
324+
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
325+
source_dist.unpack(sdist_tree.path()).unwrap();
326+
let sdist_top_level_directory = sdist_tree.path().join(format!(
327+
"{}-{}",
328+
source_dist_filename.name.as_dist_info_name(),
329+
source_dist_filename.version
330+
));
331+
let wheel_filename =
332+
build_wheel(&sdist_top_level_directory, dist, None, "1.0.0+test").unwrap();
333+
let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
334+
335+
// Check that direct and indirect wheels are identical.
336+
assert_eq!(direct_wheel_filename, wheel_filename);
337+
assert_eq!(direct_wheel_contents, wheel_contents);
338+
assert_eq!(direct_wheel_list_files, wheel_list_files);
339+
assert_eq!(
340+
direct_wheel_hash,
341+
sha2::Sha256::digest(fs_err::read(dist.join(wheel_filename.to_string())).unwrap())
342+
);
343+
344+
BuildResults {
345+
source_dist_list_files,
346+
source_dist_filename,
347+
source_dist_contents,
348+
wheel_list_files,
349+
wheel_filename,
350+
wheel_contents,
351+
}
352+
}
353+
354+
fn sdist_contents(source_dist_path: &Path) -> Vec<String> {
355+
let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap());
356+
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
357+
let mut source_dist_contents: Vec<_> = source_dist
358+
.entries()
359+
.unwrap()
360+
.map(|entry| {
361+
entry
362+
.unwrap()
363+
.path()
364+
.unwrap()
365+
.to_str()
366+
.unwrap()
367+
.replace('\\', "/")
368+
})
369+
.collect();
370+
source_dist_contents.sort();
371+
source_dist_contents
372+
}
373+
374+
fn wheel_contents(direct_output_dir: &Path) -> Vec<String> {
375+
let wheel = zip::ZipArchive::new(File::open(direct_output_dir).unwrap()).unwrap();
376+
let mut wheel_contents: Vec<_> = wheel
377+
.file_names()
378+
.map(|path| path.replace('\\', "/"))
379+
.collect();
380+
wheel_contents.sort_unstable();
381+
wheel_contents
382+
}
383+
384+
fn format_file_list(file_list: FileList, src: &Path) -> String {
385+
file_list
386+
.into_iter()
387+
.map(|(path, source)| {
388+
let path = path.replace('\\', "/");
389+
if let Some(source) = source {
390+
let source = relative_to(source, src)
391+
.unwrap()
392+
.portable_display()
393+
.to_string();
394+
format!("{path} ({source})")
395+
} else {
396+
format!("{path} (generated)")
397+
}
398+
})
399+
.join("\n")
400+
}
401+
286402
/// Tests that builds are stable and include the right files and.
287403
///
288404
/// Tests that both source tree -> source dist -> wheel and source tree -> wheel include the
@@ -335,91 +451,39 @@ mod tests {
335451
File::create(module_root.join("__pycache__").join("compiled.pyc")).unwrap();
336452
File::create(module_root.join("arithmetic").join("circle.pyc")).unwrap();
337453

338-
// Build a wheel from the source tree
339-
let direct_output_dir = TempDir::new().unwrap();
340-
let (_name, wheel_list_files) = list_wheel(src.path(), "1.0.0+test").unwrap();
341-
build_wheel(src.path(), direct_output_dir.path(), None, "1.0.0+test").unwrap();
454+
// Perform both the direct and the indirect build.
455+
let dist = TempDir::new().unwrap();
456+
let build = build(src.path(), dist.path());
342457

343-
let wheel = zip::ZipArchive::new(
344-
File::open(
345-
direct_output_dir
346-
.path()
347-
.join("built_by_uv-0.1.0-py3-none-any.whl"),
348-
)
349-
.unwrap(),
350-
)
351-
.unwrap();
352-
let mut direct_wheel_contents: Vec<_> = wheel.file_names().collect();
353-
direct_wheel_contents.sort_unstable();
354-
355-
// List file and build a source dist from the source tree
356-
let source_dist_dir = TempDir::new().unwrap();
357-
let (_name, source_dist_list_files) = list_source_dist(src.path(), "1.0.0+test").unwrap();
358-
build_source_dist(src.path(), source_dist_dir.path(), "1.0.0+test").unwrap();
359-
let source_dist_path = source_dist_dir.path().join("built_by_uv-0.1.0.tar.gz");
458+
let source_dist_path = dist.path().join(build.source_dist_filename.to_string());
459+
assert_eq!(
460+
build.source_dist_filename.to_string(),
461+
"built_by_uv-0.1.0.tar.gz"
462+
);
360463
// Check that the source dist is reproducible across platforms.
361464
assert_snapshot!(
362465
format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())),
363466
@"dab46bcc4d66960a11cfdc19604512a8e1a3241a67536f7e962166760e9c575c"
364467
);
365-
366-
// Build a wheel from the source dist
367-
let sdist_tree = TempDir::new().unwrap();
368-
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
369-
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
370-
let mut source_dist_contents: Vec<_> = source_dist
371-
.entries()
372-
.unwrap()
373-
.map(|entry| entry.unwrap().path().unwrap().to_str().unwrap().to_string())
374-
.collect();
375-
source_dist_contents.sort();
376-
// Reset the reader and unpack
377-
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
378-
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
379-
source_dist.unpack(sdist_tree.path()).unwrap();
380-
drop(source_dist_dir);
381-
382-
let indirect_output_dir = TempDir::new().unwrap();
383-
build_wheel(
384-
&sdist_tree.path().join("built_by_uv-0.1.0"),
385-
indirect_output_dir.path(),
386-
None,
387-
"1.0.0+test",
388-
)
389-
.unwrap();
390-
let wheel = zip::ZipArchive::new(
391-
File::open(
392-
indirect_output_dir
393-
.path()
394-
.join("built_by_uv-0.1.0-py3-none-any.whl"),
395-
)
396-
.unwrap(),
397-
)
398-
.unwrap();
399-
let mut indirect_wheel_contents: Vec<_> = wheel.file_names().collect();
400-
indirect_wheel_contents.sort_unstable();
401-
assert_eq!(indirect_wheel_contents, direct_wheel_contents);
402-
403-
let format_file_list = |file_list: FileList| {
404-
file_list
405-
.into_iter()
406-
.map(|(path, source)| {
407-
let path = path.replace('\\', "/");
408-
if let Some(source) = source {
409-
let source = relative_to(source, src.path())
410-
.unwrap()
411-
.portable_display()
412-
.to_string();
413-
format!("{path} ({source})")
414-
} else {
415-
format!("{path} (generated)")
416-
}
417-
})
418-
.join("\n")
419-
};
420-
421-
// Check the contained files and directories
422-
assert_snapshot!(source_dist_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r###"
468+
// Check both the files we report and the actual files
469+
assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @r"
470+
built_by_uv-0.1.0/PKG-INFO (generated)
471+
built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
472+
built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
473+
built_by_uv-0.1.0/README.md (README.md)
474+
built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
475+
built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
476+
built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
477+
built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
478+
built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
479+
built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
480+
built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
481+
built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
482+
built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
483+
built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
484+
built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
485+
");
486+
assert_snapshot!(build.source_dist_contents.iter().join("\n"), @r"
423487
built_by_uv-0.1.0/
424488
built_by_uv-0.1.0/LICENSE-APACHE
425489
built_by_uv-0.1.0/LICENSE-MIT
@@ -443,26 +507,19 @@ mod tests {
443507
built_by_uv-0.1.0/src/built_by_uv/cli.py
444508
built_by_uv-0.1.0/third-party-licenses
445509
built_by_uv-0.1.0/third-party-licenses/PEP-401.txt
446-
"###);
447-
assert_snapshot!(format_file_list(source_dist_list_files), @r"
448-
built_by_uv-0.1.0/PKG-INFO (generated)
449-
built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
450-
built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
451-
built_by_uv-0.1.0/README.md (README.md)
452-
built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
453-
built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
454-
built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
455-
built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
456-
built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
457-
built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
458-
built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
459-
built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
460-
built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
461-
built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
462-
built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
463510
");
464511

465-
assert_snapshot!(indirect_wheel_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r###"
512+
let wheel_path = dist.path().join(build.wheel_filename.to_string());
513+
assert_eq!(
514+
build.wheel_filename.to_string(),
515+
"built_by_uv-0.1.0-py3-none-any.whl"
516+
);
517+
// Check that the wheel is reproducible across platforms.
518+
assert_snapshot!(
519+
format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())),
520+
@"ac3f68ac448023bca26de689d80401bff57f764396ae802bf4666234740ffbe3"
521+
);
522+
assert_snapshot!(build.wheel_contents.join("\n"), @r"
466523
built_by_uv-0.1.0.data/data/
467524
built_by_uv-0.1.0.data/data/data.csv
468525
built_by_uv-0.1.0.data/headers/
@@ -486,9 +543,8 @@ mod tests {
486543
built_by_uv/arithmetic/circle.py
487544
built_by_uv/arithmetic/pi.txt
488545
built_by_uv/cli.py
489-
"###);
490-
491-
assert_snapshot!(format_file_list(wheel_list_files), @r"
546+
");
547+
assert_snapshot!(format_file_list(build.wheel_list_files, src.path()), @r"
492548
built_by_uv/__init__.py (src/built_by_uv/__init__.py)
493549
built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
494550
built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
@@ -504,19 +560,6 @@ mod tests {
504560
built_by_uv-0.1.0.dist-info/entry_points.txt (generated)
505561
built_by_uv-0.1.0.dist-info/METADATA (generated)
506562
");
507-
508-
// Check that the wheel is the same for both build paths and reproducible across platforms.
509-
let wheel_filename = "built_by_uv-0.1.0-py3-none-any.whl";
510-
let index_wheel_contents =
511-
fs_err::read(indirect_output_dir.path().join(wheel_filename)).unwrap();
512-
assert_eq!(
513-
fs_err::read(direct_output_dir.path().join(wheel_filename)).unwrap(),
514-
index_wheel_contents
515-
);
516-
assert_snapshot!(
517-
format!("{:x}", sha2::Sha256::digest(&index_wheel_contents)),
518-
@"ac3f68ac448023bca26de689d80401bff57f764396ae802bf4666234740ffbe3"
519-
);
520563
}
521564

522565
/// Test that `license = { file = "LICENSE" }` is supported.

0 commit comments

Comments
 (0)