#
cargo_test_support/
publish.rs

    1//! Helpers for testing `cargo package` / `cargo publish`
    2//!
    3//! # Example
    4//!
    5//! ```no_run
    6//! # use cargo_test_support::registry::RegistryBuilder;
    7//! # use cargo_test_support::publish::validate_upload;
    8//! # use cargo_test_support::project;
    9//! // This replaces `registry::init()` and must be called before `Package::new().publish()`
    10//! let registry = RegistryBuilder::new().http_api().http_index().build();
    11//!
    12//! let p = project()
    13//!     .file(
    14//!         "Cargo.toml",
    15//!         r#"
    16//!             [package]
    17//!             name = "foo"
    18//!             version = "0.0.1"
    19//!             edition = "2015"
    20//!             authors = []
    21//!             license = "MIT"
    22//!             description = "foo"
    23//!         "#,
    24//!     )
    25//!     .file("src/main.rs", "fn main() {}")
    26//!     .build();
    27//!
    28//! p.cargo("publish --no-verify")
    29//!     .replace_crates_io(registry.index_url())
    30//!     .run();
    31//!
    32//! validate_upload(
    33//!     r#"
    34//!     {
    35//!       "authors": [],
    36//!       "badges": {},
    37//!       "categories": [],
    38//!       "deps": [],
    39//!       "description": "foo",
    40//!       "documentation": null,
    41//!       "features": {},
    42//!       "homepage": null,
    43//!       "keywords": [],
    44//!       "license": "MIT",
    45//!       "license_file": null,
    46//!       "links": null,
    47//!       "name": "foo",
    48//!       "readme": null,
    49//!       "readme_file": null,
    50//!       "repository": null,
    51//!       "rust_version": null,
    52//!       "vers": "0.0.1"
    53//!       }
    54//!     "#,
    55//!     "foo-0.0.1.crate",
    56//!     &["Cargo.lock", "Cargo.toml", "Cargo.toml.orig", "src/main.rs"],
    57//! );
    58//! ```
    59
    60use crate::compare::InMemoryDir;
    61use crate::registry::{self, alt_api_path, FeatureMap};
    62use flate2::read::GzDecoder;
    63use snapbox::prelude::*;
    64use std::collections::HashSet;
    65use std::fs;
    66use std::fs::File;
    67use std::io::{self, prelude::*, SeekFrom};
    68use std::path::Path;
    69use tar::Archive;
    70
    71fn read_le_u32<R>(mut reader: R) -> io::Result<u32>
    72where
    73    R: Read,
    74{
    75    let mut buf = [0; 4];
    76    reader.read_exact(&mut buf)?;
    77    Ok(u32::from_le_bytes(buf))
    78}
    79
    80/// Check the `cargo publish` API call
    81#[track_caller]
    82pub fn validate_upload(expected_json: &str, expected_crate_name: &str, expected_files: &[&str]) {
    83    let new_path = registry::api_path().join("api/v1/crates/new");
    84    _validate_upload(
    85        &new_path,
    86        expected_json,
    87        expected_crate_name,
    88        expected_files,
    89        (),
    90    );
    91}
    92
    93/// Check the `cargo publish` API call, with file contents
    94#[track_caller]
    95pub fn validate_upload_with_contents(
    96    expected_json: &str,
    97    expected_crate_name: &str,
    98    expected_files: &[&str],
    99    expected_contents: impl Into<InMemoryDir>,
    100) {
    101    let new_path = registry::api_path().join("api/v1/crates/new");
    102    _validate_upload(
    103        &new_path,
    104        expected_json,
    105        expected_crate_name,
    106        expected_files,
    107        expected_contents,
    108    );
    109}
    110
    111/// Check the `cargo publish` API call to the alternative test registry
    112#[track_caller]
    113pub fn validate_alt_upload(
    114    expected_json: &str,
    115    expected_crate_name: &str,
    116    expected_files: &[&str],
    117) {
    118    let new_path = alt_api_path().join("api/v1/crates/new");
    119    _validate_upload(
    120        &new_path,
    121        expected_json,
    122        expected_crate_name,
    123        expected_files,
    124        (),
    125    );
    126}
    127
    128#[track_caller]
    129fn _validate_upload(
    130    new_path: &Path,
    131    expected_json: &str,
    132    expected_crate_name: &str,
    133    expected_files: &[&str],
    134    expected_contents: impl Into<InMemoryDir>,
    135) {
    136    let (actual_json, krate_bytes) = read_new_post(new_path);
    137
    138    snapbox::assert_data_eq!(actual_json, expected_json.is_json());
    139
    140    // Verify the tarball.
    141    validate_crate_contents(
    142        &krate_bytes[..],
    143        expected_crate_name,
    144        expected_files,
    145        expected_contents,
    146    );
    147}
    148
    149#[track_caller]
    150fn read_new_post(new_path: &Path) -> (Vec<u8>, Vec<u8>) {
    151    let mut f = File::open(new_path).unwrap();
    152
    153    // 32-bit little-endian integer of length of JSON data.
    154    let json_sz = read_le_u32(&mut f).expect("read json length");
    155    let mut json_bytes = vec![0; json_sz as usize];
    156    f.read_exact(&mut json_bytes).expect("read JSON data");
    157
    158    // 32-bit little-endian integer of length of crate file.
    159    let crate_sz = read_le_u32(&mut f).expect("read crate length");
    160    let mut krate_bytes = vec![0; crate_sz as usize];
    161    f.read_exact(&mut krate_bytes).expect("read crate data");
    162
    163    // Check at end.
    164    let current = f.seek(SeekFrom::Current(0)).unwrap();
    165    assert_eq!(f.seek(SeekFrom::End(0)).unwrap(), current);
    166
    167    (json_bytes, krate_bytes)
    168}
    169
    170/// Checks the contents of a `.crate` file.
    171///
    172/// - `expected_crate_name` should be something like `foo-0.0.1.crate`.
    173/// - `expected_files` should be a complete list of files in the crate
    174///   (relative to `expected_crate_name`).
    175/// - `expected_contents` should be a list of `(file_name, contents)` tuples
    176///   to validate the contents of the given file. Only the listed files will
    177///   be checked (others will be ignored).
    178#[track_caller]
    179pub fn validate_crate_contents(
    180    reader: impl Read,
    181    expected_crate_name: &str,
    182    expected_files: &[&str],
    183    expected_contents: impl Into<InMemoryDir>,
    184) {
    185    let expected_contents = expected_contents.into();
    186    validate_crate_contents_(
    187        reader,
    188        expected_crate_name,
    189        expected_files,
    190        expected_contents,
    191    )
    192}
    193
    194#[track_caller]
    195fn validate_crate_contents_(
    196    reader: impl Read,
    197    expected_crate_name: &str,
    198    expected_files: &[&str],
    199    expected_contents: InMemoryDir,
    200) {
    201    let mut rdr = GzDecoder::new(reader);
    202    snapbox::assert_data_eq!(rdr.header().unwrap().filename().unwrap(), {
    203        let expected: snapbox::Data = expected_crate_name.into();
    204        expected.raw()
    205    });
    206
    207    let mut contents = Vec::new();
    208    rdr.read_to_end(&mut contents).unwrap();
    209    let mut ar = Archive::new(&contents[..]);
    210    let base_crate_name = Path::new(
    211        expected_crate_name
    212            .strip_suffix(".crate")
    213            .expect("must end with .crate"),
    214    );
    215    let actual_contents: InMemoryDir = ar
    216        .entries()
    217        .unwrap()
    218        .map(|entry| {
    219            let mut entry = entry.unwrap();
    220            let name = entry
    221                .path()
    222                .unwrap()
    223                .strip_prefix(base_crate_name)
    224                .unwrap()
    225                .to_owned();
    226            let mut contents = String::new();
    227            entry.read_to_string(&mut contents).unwrap();
    228            (name, contents)
    229        })
    230        .collect();
    231    let actual_files: HashSet<&Path> = actual_contents.paths().collect();
    232    let expected_files: HashSet<&Path> =
    233        expected_files.iter().map(|name| Path::new(name)).collect();
    234    let missing: Vec<&&Path> = expected_files.difference(&actual_files).collect();
    235    let extra: Vec<&&Path> = actual_files.difference(&expected_files).collect();
    236    if !missing.is_empty() || !extra.is_empty() {
    237        panic!(
    238            "uploaded archive does not match.\nMissing: {:?}\nExtra: {:?}\n",
    239            missing, extra
    240        );
    241    }
    242    actual_contents.assert_contains(&expected_contents);
    243}
    244
    245pub(crate) fn create_index_line(
    246    name: serde_json::Value,
    247    vers: &str,
    248    deps: Vec<serde_json::Value>,
    249    cksum: &str,
    250    features: crate::registry::FeatureMap,
    251    yanked: bool,
    252    links: Option<String>,
    253    rust_version: Option<&str>,
    254    v: Option<u32>,
    255) -> String {
    256    // This emulates what crates.io does to retain backwards compatibility.
    257    let (features, features2) = split_index_features(features.clone());
    258    let mut json = serde_json::json!({
    259        "name": name,
    260        "vers": vers,
    261        "deps": deps,
    262        "cksum": cksum,
    263        "features": features,
    264        "yanked": yanked,
    265        "links": links,
    266    });
    267    if let Some(f2) = &features2 {
    268        json["features2"] = serde_json::json!(f2);
    269        json["v"] = serde_json::json!(2);
    270    }
    271    if let Some(v) = v {
    272        json["v"] = serde_json::json!(v);
    273    }
    274    if let Some(rust_version) = rust_version {
    275        json["rust_version"] = serde_json::json!(rust_version);
    276    }
    277
    278    json.to_string()
    279}
    280
    281pub(crate) fn write_to_index(registry_path: &Path, name: &str, line: String, local: bool) {
    282    let file = cargo_util::registry::make_dep_path(name, false);
    283
    284    // Write file/line in the index.
    285    let dst = if local {
    286        registry_path.join("index").join(&file)
    287    } else {
    288        registry_path.join(&file)
    289    };
    290    let prev = fs::read_to_string(&dst).unwrap_or_default();
    291    t!(fs::create_dir_all(dst.parent().unwrap()));
    292    t!(fs::write(&dst, prev + &line[..] + "\n"));
    293
    294    // Add the new file to the index.
    295    if !local {
    296        let repo = t!(git2::Repository::open(&registry_path));
    297        let mut index = t!(repo.index());
    298        t!(index.add_path(Path::new(&file)));
    299        t!(index.write());
    300        let id = t!(index.write_tree());
    301
    302        // Commit this change.
    303        let tree = t!(repo.find_tree(id));
    304        let sig = t!(repo.signature());
    305        let parent = t!(repo.refname_to_id("refs/heads/master"));
    306        let parent = t!(repo.find_commit(parent));
    307        t!(repo.commit(
    308            Some("HEAD"),
    309            &sig,
    310            &sig,
    311            "Another commit",
    312            &tree,
    313            &[&parent]
    314        ));
    315    }
    316}
    317
    318fn split_index_features(mut features: FeatureMap) -> (FeatureMap, Option<FeatureMap>) {
    319    let mut features2 = FeatureMap::new();
    320    for (feat, values) in features.iter_mut() {
    321        if values
    322            .iter()
    323            .any(|value| value.starts_with("dep:") || value.contains("?/"))
    324        {
    325            let new_values = std::mem::take(values);
    326            features2.insert(feat.clone(), new_values);
    327        }
    328    }
    329    if features2.is_empty() {
    330        (features, None)
    331    } else {
    332        (features, Some(features2))
    333    }
    334}