From a60c9638a7709a603b839219a1c70239bb73d33c Mon Sep 17 00:00:00 2001 From: RishabhSaini Date: Mon, 16 Jan 2023 17:33:17 -0500 Subject: [PATCH] chunking: Bin packing algorithm which allows to minimize layer deltas using historical builds Revamp basic_packing to follow the prior packing structure if the --prior-build flag exists. This simply modifies existing layers with upgrades/downgrades/removal of packages. The last layer contains any new addition to packages. In the case where --prior-build flag does not exist, the frequency of updates of the packages (frequencyinfo) and size is utilized to segment packages into different partitions (all combinations of low, medium, high frequency and low, medium, high size). The partition that each package falls into is decided by its deviation from mean. Then the packages are alloted to different layers to ensure 1) low frequency packages don't mix with high frequency packages 2) High sized packages are alloted separate bins 3) Low sized packages can be put together in the same bin This problem is aka multi-objective bin packing problem with constraints aka multiple knapsack problem. The objectives are conflicting given our constraints and hence a compromise is taken to minimize layer deltas while respecting the hard limit of overlayfs that the kernel can handle. --- lib/src/chunking.rs | 519 ++++++++++++++++-- lib/src/container/store.rs | 7 +- lib/src/fixture.rs | 3 +- .../fedora-coreos-contentmeta.json.gz | Bin 10233 -> 11361 bytes lib/src/objectsource.rs | 3 +- lib/tests/it/main.rs | 17 +- 6 files changed, 475 insertions(+), 74 deletions(-) diff --git a/lib/src/chunking.rs b/lib/src/chunking.rs index 33e78c63..bedb59c8 100644 --- a/lib/src/chunking.rs +++ b/lib/src/chunking.rs @@ -3,10 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use std::borrow::{Borrow, Cow}; -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Write; +use std::hash::{Hash, Hasher}; use std::num::NonZeroU32; use std::rc::Rc; +use std::time::Instant; use crate::objectsource::{ContentID, ObjectMeta, ObjectMetaMap, ObjectSourceMeta}; use crate::objgv::*; @@ -43,6 +45,20 @@ pub struct ObjectSourceMetaSized { size: u64, } +impl Hash for ObjectSourceMetaSized { + fn hash(&self, state: &mut H) { + self.meta.identifier.hash(state); + } +} + +impl Eq for ObjectSourceMetaSized {} + +impl PartialEq for ObjectSourceMetaSized { + fn eq(&self, other: &Self) -> bool { + self.meta.identifier == other.meta.identifier + } +} + /// Extend content source metadata with sizes. #[derive(Debug)] pub struct ObjectMetaSized { @@ -294,20 +310,27 @@ impl Chunking { .unwrap(); // TODO: Compute bin packing in a better way + let start = Instant::now(); let packing = basic_packing( sizes, NonZeroU32::new(self.max).unwrap(), prior_build_metadata, ); + let duration = start.elapsed(); + println!("Time elapsed in packing: {:#?}", duration); for bin in packing.into_iter() { - let first = bin[0]; - let first_name = &*first.meta.name; let name = match bin.len() { - 0 => unreachable!(), - 1 => Cow::Borrowed(first_name), + 0 => Cow::Borrowed("Reserved for new packages"), + 1 => { + let first = bin[0]; + let first_name = &*first.meta.identifier; + Cow::Borrowed(first_name) + } 2..=5 => { - let r = bin.iter().map(|v| &*v.meta.name).skip(1).fold( + let first = bin[0]; + let first_name = &*first.meta.identifier; + let r = bin.iter().map(|v| &*v.meta.identifier).skip(1).fold( String::from(first_name), |mut acc, v| { write!(acc, " and {}", v).unwrap(); @@ -325,9 +348,7 @@ impl Chunking { self.remainder.move_obj(&mut chunk, obj.as_str()); } } - if !chunk.content.is_empty() { - self.chunks.push(chunk); - } + self.chunks.push(chunk); } assert_eq!(self.remainder.content.len(), 0); @@ -372,82 +393,460 @@ impl Chunking { } } -type ChunkedComponents<'a> = Vec<&'a ObjectSourceMetaSized>; - +#[cfg(test)] fn components_size(components: &[&ObjectSourceMetaSized]) -> u64 { components.iter().map(|k| k.size).sum() } /// Compute the total size of a packing #[cfg(test)] -fn packing_size(packing: &[ChunkedComponents]) -> u64 { +fn packing_size(packing: &[Vec<&ObjectSourceMetaSized>]) -> u64 { packing.iter().map(|v| components_size(v)).sum() } -fn sort_packing(packing: &mut [ChunkedComponents]) { - packing.sort_by(|a, b| { - let a: u64 = components_size(a); - let b: u64 = components_size(b); - b.cmp(&a) +fn mean(data: &[u64]) -> Option { + if data.is_empty() { + None + } else { + Some((data.iter().sum::() / data.len() as u64) as f64) + } +} + +fn std_deviation(data: &[u64]) -> Option { + match (mean(data), data.len()) { + (Some(data_mean), count) if count > 0 => { + let variance = data + .iter() + .map(|value| { + let diff = data_mean - (*value as f64); + diff * diff + }) + .sum::() + / count as f64; + Some(variance.sqrt()) + } + _ => None, + } +} + +fn median_absolute_deviation(data: &mut Vec) -> (f64, f64) { + //Sort data + //data.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + //Find median of data + let median_data: f64 = match data.len() % 2 { + 1 => data[data.len() / 2] as f64, + _ => 0.5 * (data[data.len() / 2 - 1] + data[data.len() / 2]) as f64, + }; + + //Absolute deviations + let mut absolute_deviations = Vec::new(); + for size in data { + absolute_deviations.push(f64::abs(*size as f64 - median_data)) + } + + absolute_deviations.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let l = absolute_deviations.len(); + let mad: f64 = match l % 2 { + 1 => absolute_deviations[l / 2], + _ => 0.5 * (absolute_deviations[l / 2 - 1] + absolute_deviations[l / 2]), + }; + + (median_data, mad) +} + +///Given a certain threshold, divide a list of packages into all combinations +///of (high, medium, low) size and (high,medium,low) using the following +///outlier detection methods: +///- Median and Median Absolute Deviation Method +/// Aggressively detects outliers in size and classifies them by +/// high, medium, low. The high size and low size are separate partitions +/// and deserve bins of their own +///- Mean and Standard Deviation Method +/// The medium partition from the previous step is less aggressively +/// classified by using mean for both size and frequency + +//Assumes components is sorted by descending size +fn get_partitions_with_threshold( + components: Vec<&ObjectSourceMetaSized>, + limit_hs_bins: usize, + threshold: f64, +) -> Option>> { + let mut partitions: BTreeMap> = BTreeMap::new(); + let mut med_size: Vec<&ObjectSourceMetaSized> = Vec::new(); + let mut high_size: Vec<&ObjectSourceMetaSized> = Vec::new(); + + let mut sizes: Vec = components.iter().map(|a| a.size).collect(); + let (median_size, mad_size) = median_absolute_deviation(&mut sizes); + + //Avoids lower limit being negative + let size_low_limit = 0.5 * f64::abs(median_size - threshold * mad_size); + let size_high_limit = median_size + threshold * mad_size; + + for pkg in components { + let size = pkg.size as f64; + + //high size (hs) + if size >= size_high_limit { + high_size.push(pkg); + } + //low size (ls) + else if size <= size_low_limit { + partitions + .entry("2ls".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //medium size (ms) + else { + med_size.push(pkg); + } + } + + //Extra hs packages + let mut remaining_pkgs: Vec<_> = high_size.drain(limit_hs_bins..).collect(); + assert_eq!(high_size.len(), limit_hs_bins); + + //Concatenate extra hs packages + med_sizes to keep it descending sorted + remaining_pkgs.append(&mut med_size); + partitions.insert("1hs".to_string(), high_size); + + //Ascending sorted by frequency, so each partition within ms is freq sorted + remaining_pkgs.sort_by(|a, b| { + a.meta + .change_frequency + .partial_cmp(&b.meta.change_frequency) + .unwrap() }); + let med_sizes: Vec = remaining_pkgs.iter().map(|a| a.size).collect(); + let med_frequencies: Vec = remaining_pkgs + .iter() + .map(|a| a.meta.change_frequency.into()) + .collect(); + + let med_mean_freq = mean(&med_frequencies)?; + let med_stddev_freq = std_deviation(&med_frequencies)?; + let med_mean_size = mean(&med_sizes)?; + let med_stddev_size = std_deviation(&med_sizes)?; + + //Avoids lower limit being negative + let med_freq_low_limit = 0.5f64 * f64::abs(med_mean_freq - threshold * med_stddev_freq); + let med_freq_high_limit = med_mean_freq + threshold * med_stddev_freq; + let med_size_low_limit = 0.5f64 * f64::abs(med_mean_size - threshold * med_stddev_size); + let med_size_high_limit = med_mean_size + threshold * med_stddev_size; + + for pkg in remaining_pkgs { + let size = pkg.size as f64; + let freq = pkg.meta.change_frequency as f64; + + //low frequency, high size + if (freq <= med_freq_low_limit) && (size >= med_size_high_limit) { + partitions + .entry("lf_hs".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //medium frequency, high size + else if (freq < med_freq_high_limit) + && (freq > med_freq_low_limit) + && (size >= med_size_high_limit) + { + partitions + .entry("mf_hs".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //high frequency, high size + else if (freq >= med_freq_high_limit) && (size >= med_size_high_limit) { + partitions + .entry("hf_hs".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //low frequency, medium size + else if (freq <= med_freq_low_limit) + && (size < med_size_high_limit) + && (size > med_size_low_limit) + { + partitions + .entry("lf_ms".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //medium frequency, medium size + else if (freq < med_freq_high_limit) + && (freq > med_freq_low_limit) + && (size < med_size_high_limit) + && (size > med_size_low_limit) + { + partitions + .entry("mf_ms".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //high frequency, medium size + else if (freq >= med_freq_high_limit) + && (size < med_size_high_limit) + && (size > med_size_low_limit) + { + partitions + .entry("hf_ms".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //low frequency, low size + else if (freq <= med_freq_low_limit) && (size <= med_size_low_limit) { + partitions + .entry("lf_ls".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //medium frequency, low size + else if (freq < med_freq_high_limit) + && (freq > med_freq_low_limit) + && (size <= med_size_low_limit) + { + partitions + .entry("mf_ls".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + //high frequency, low size + else if (freq >= med_freq_high_limit) && (size <= med_size_low_limit) { + partitions + .entry("hf_ls".to_string()) + .and_modify(|bin| bin.push(pkg)) + .or_insert_with(|| vec![pkg]); + } + } + + for (name, pkgs) in &partitions { + println!("{:#?}: {:#?}", name, pkgs.len()); + } + + Some(partitions) } /// Given a set of components with size metadata (e.g. boxes of a certain size) /// and a number of bins (possible container layers) to use, determine which components /// go in which bin. This algorithm is pretty simple: -/// -/// - order by size -/// - If we have fewer components than bins, we're done -/// - Take the "tail" (all components past maximum), and group by source package -/// - If we have fewer components than bins, we're done -/// - Take the whole tail and group them toether (this is the overly simplistic part) + +// Total available bins = n +// +// 1 bin for all the u32_max frequency pkgs +// 1 bin for all newly added pkgs +// 1 bin for all low size pkgs +// +// 60% of n-3 bins for high size pkgs +// 40% of n-3 bins for medium size pkgs +// +// If HS bins > limit, spillover to MS to package +// If MS bins > limit, fold by merging 2 bins from the end +// fn basic_packing<'a>( components: &'a [ObjectSourceMetaSized], - bins: NonZeroU32, + bin_size: NonZeroU32, prior_build_metadata: &'a Option>>, -) -> Vec> { - // let total_size: u64 = components.iter().map(|v| v.size).sum(); - // let avg_size: u64 = total_size / components.len() as u64; +) -> Vec> { let mut r = Vec::new(); - // And handle the easy case of enough bins for all components - // TODO: Possibly try to split off large files? - if components.len() <= bins.get() as usize { - r.extend(components.iter().map(|v| vec![v])); - return r; - } - // Create a mutable copy let mut components: Vec<_> = components.iter().collect(); - // Iterate over the component tail, folding by source id - let mut by_src = HashMap::<_, Vec<&ObjectSourceMetaSized>>::new(); - // Take the tail off components, then build up mapping from srcid -> Vec - for component in components.split_off(bins.get() as usize) { - by_src - .entry(&component.meta.srcid) - .or_default() - .push(component); + let before_processing_pkgs_len = components.len(); + if before_processing_pkgs_len == 0 { + return Vec::new(); } - // Take all the non-tail (largest) components, and append them first - r.extend(components.into_iter().map(|v| vec![v])); - // Add the tail - r.extend(by_src.into_values()); - // And order the new list - sort_packing(&mut r); - // It's possible that merging components gave us enough space; if so - // we're done! - if r.len() <= bins.get() as usize { - return r; + //Flatten out prior_build_metadata[i] to view all the packages in prior build as a single vec + // + //If the current rpm-ostree commit to be encapsulated is not the one in which packing structure changes, then + // Compare flatten(prior_build_metadata[i]) to components to see if pkgs added, updated, + // removed or kept same + // if pkgs added, then add them to the last bin of prior[i][n] + // if pkgs removed, then remove them from the prior[i] + // iterate through prior[i] and make bins according to the name in nevra of pkgs and return + // (no need of recomputing packaging structure) + //else if pkg structure to be changed || prior build not specified + // Recompute optimal packaging strcuture (Compute partitions, place packages and optimize build) + + if let Some(prior_build) = prior_build_metadata + /* && structure not be changed*/ + { + println!("Keeping old package structure"); + let mut curr_build: Vec> = prior_build.clone(); + //Packing only manaages RPMs not OStree commit + curr_build.remove(0); + let mut prev_pkgs: Vec = Vec::new(); + for bin in &curr_build { + for pkg in bin { + prev_pkgs.push(pkg.to_string()); + } + } + prev_pkgs.retain(|name| !name.is_empty()); + let curr_pkgs: Vec = components + .iter() + .map(|pkg| pkg.meta.name.to_string()) + .collect(); + let prev_pkgs_set: HashSet = HashSet::from_iter(prev_pkgs); + let curr_pkgs_set: HashSet = HashSet::from_iter(curr_pkgs); + let added: HashSet<&String> = curr_pkgs_set.difference(&prev_pkgs_set).collect(); + let removed: HashSet<&String> = prev_pkgs_set.difference(&curr_pkgs_set).collect(); + let mut add_pkgs_v: Vec = Vec::new(); + for pkg in added { + add_pkgs_v.push(pkg.to_string()); + } + let mut rem_pkgs_v: Vec = Vec::new(); + for pkg in removed { + rem_pkgs_v.push(pkg.to_string()); + } + let curr_build_len = &curr_build.len(); + curr_build[curr_build_len - 1].retain(|name| !name.is_empty()); + curr_build[curr_build_len - 1].extend(add_pkgs_v); + for bin in curr_build.iter_mut() { + bin.retain(|pkg| !rem_pkgs_v.contains(pkg)); + } + let mut name_to_component: HashMap = HashMap::new(); + for component in &components { + name_to_component + .entry(component.meta.name.to_string()) + .or_insert(component); + } + let mut modified_build: Vec> = Vec::new(); + for bin in curr_build { + let mut mod_bin = Vec::new(); + for pkg in bin { + mod_bin.push(name_to_component[&pkg]); + } + modified_build.push(mod_bin); + } + let mut after_processing_pkgs_len = 0; + modified_build.iter().for_each(|bin| { + after_processing_pkgs_len += bin.len(); + }); + assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len); + assert!(modified_build.len() <= bin_size.get() as usize); + return modified_build; } - let last = (bins.get().checked_sub(1).unwrap()) as usize; - // The "tail" is components past our maximum. For now, we simply group all of that together as a single unit. - if let Some(tail) = r.drain(last..).reduce(|mut a, b| { - a.extend(b.into_iter()); - a - }) { - r.push(tail); + println!("Creating new packing structure"); + + let mut max_freq_components: Vec<&ObjectSourceMetaSized> = Vec::new(); + components.retain(|pkg| { + let retain: bool = pkg.meta.change_frequency != u32::MAX; + if !retain { + max_freq_components.push(pkg); + } + retain + }); + let components_len_after_max_freq = components.len(); + match components_len_after_max_freq { + 0 => (), + _ => { + //Defining Limits of each bins + let limit_ls_bins = 1usize; + let limit_new_bins = 1usize; + let _limit_new_pkgs = 0usize; + let limit_max_frequency_bins = 1usize; + let _limit_max_frequency_pkgs = max_freq_components.len(); + let limit_hs_bins = (0.6 + * (bin_size.get() + - (limit_ls_bins + limit_new_bins + limit_max_frequency_bins) as u32) + as f32) + .floor() as usize; + let limit_ms_bins = (0.4 + * (bin_size.get() + - (limit_ls_bins + limit_new_bins + limit_max_frequency_bins) as u32) + as f32) + .floor() as usize; + + let partitions = + get_partitions_with_threshold(components, limit_hs_bins as usize, 2f64) + .expect("Partitioning components into sets"); + + let limit_ls_pkgs = match partitions.get("2ls") { + Some(n) => n.len(), + None => 0usize, + }; + + let pkg_per_bin_ms: usize = + match (components_len_after_max_freq - limit_hs_bins - limit_ls_pkgs) + .checked_div(limit_ms_bins) + { + Some(n) => { + if n < 1 { + panic!("Error: No of bins <= 3"); + } + n + } + None => { + panic!("Error: No of bins <= 3") + } + }; + + //Bins assignment + for partition in partitions.keys() { + let pkgs = partitions.get(partition).expect("hashset"); + + if partition == "1hs" { + for pkg in pkgs { + r.push(vec![*pkg]); + } + } else if partition == "2ls" { + let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new(); + for pkg in pkgs { + bin.push(*pkg); + } + r.push(bin); + } else { + let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new(); + for (i, pkg) in pkgs.iter().enumerate() { + if bin.len() < pkg_per_bin_ms { + bin.push(*pkg); + } else { + r.push(bin.clone()); + bin.clear(); + bin.push(*pkg); + } + if i == pkgs.len() - 1 && !bin.is_empty() { + r.push(bin.clone()); + bin.clear(); + } + } + } + } + println!("Bins before unoptimized build: {}", r.len()); + + //Addressing MS bins limit breach by wrapping MS layers + while r.len() > (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins) { + for i in (limit_ls_bins + limit_hs_bins..r.len() - 1) + .step_by(2) + .rev() + { + if r.len() + <= (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins) + { + break; + } + let prev = &r[i - 1]; + let curr = &r[i]; + let mut merge: Vec<&ObjectSourceMetaSized> = Vec::new(); + merge.extend(prev.iter()); + merge.extend(curr.iter()); + r.remove(i); + r.remove(i - 1); + r.insert(i, merge); + } + } + println!("Bins after optimization: {}", r.len()); + } } + r.push(max_freq_components); - assert!(r.len() <= bins.get() as usize); + let new_pkgs_bin: Vec<&ObjectSourceMetaSized> = Vec::new(); + r.push(new_pkgs_bin); + let mut after_processing_pkgs_len = 0; + r.iter().for_each(|bin| { + after_processing_pkgs_len += bin.len(); + }); + assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len); + assert!(r.len() <= bin_size.get() as usize); r } diff --git a/lib/src/container/store.rs b/lib/src/container/store.rs index c56bc986..2c305a94 100644 --- a/lib/src/container/store.rs +++ b/lib/src/container/store.rs @@ -205,11 +205,10 @@ pub struct PreparedImport { } impl PreparedImport { - /// Iterate over all layers; the ostree split object layers, the commit layer, and any non-ostree layers. + /// Iterate over all layers; the commit layer, the ostree split object layers, and any non-ostree layers. pub fn all_layers(&self) -> impl Iterator { - self.ostree_layers - .iter() - .chain(std::iter::once(&self.ostree_commit_layer)) + std::iter::once(&self.ostree_commit_layer) + .chain(self.ostree_layers.iter()) .chain(self.layers.iter()) } diff --git a/lib/src/fixture.rs b/lib/src/fixture.rs index 614c859d..dab21e3a 100644 --- a/lib/src/fixture.rs +++ b/lib/src/fixture.rs @@ -168,7 +168,8 @@ d tmp "## }; pub const CONTENTS_CHECKSUM_V0: &str = "5e41de82f9f861fa51e53ce6dd640a260e4fb29b7657f5a3f14157e93d2c0659"; -pub static CONTENTS_V0_LEN: Lazy = Lazy::new(|| OWNERS.len().checked_sub(1).unwrap()); +// 1 for ostree commit, 2 for max frequency packages, 3 as empty layer +pub const LAYERS_V0_LEN: usize = 3usize; #[derive(Debug, PartialEq, Eq)] enum SeLabel { diff --git a/lib/src/fixtures/fedora-coreos-contentmeta.json.gz b/lib/src/fixtures/fedora-coreos-contentmeta.json.gz index a1276a3f34046a9ecd83fbc96c1c3f7fee818f6e..285d587a7c52bb3725c14a104e522af0a6d9e8be 100644 GIT binary patch literal 11361 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 jkij%pVtA+8yqlYc7R{y)c%#!Ii_^A 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 diff --git a/lib/src/objectsource.rs b/lib/src/objectsource.rs index 768104c9..a16f4dcc 100644 --- a/lib/src/objectsource.rs +++ b/lib/src/objectsource.rs @@ -39,8 +39,7 @@ pub struct ObjectSourceMeta { /// Unique identifier, does not need to be human readable, but can be. #[serde(with = "rcstr_serialize")] pub identifier: ContentID, - /// Identifier for this source (e.g. package name-version, git repo). - /// Unlike the [`ContentID`], this should be human readable. + /// Just the name of the package (no version), needs to be human readable. #[serde(with = "rcstr_serialize")] pub name: Rc, /// Identifier for the *source* of this content; for example, if multiple binary diff --git a/lib/tests/it/main.rs b/lib/tests/it/main.rs index 45928e22..432fcaef 100644 --- a/lib/tests/it/main.rs +++ b/lib/tests/it/main.rs @@ -21,7 +21,7 @@ use std::os::unix::fs::DirBuilderExt; use std::process::Command; use std::time::SystemTime; -use ostree_ext::fixture::{FileDef, Fixture, CONTENTS_CHECKSUM_V0, CONTENTS_V0_LEN}; +use ostree_ext::fixture::{FileDef, Fixture, CONTENTS_CHECKSUM_V0, LAYERS_V0_LEN}; const EXAMPLE_TAR_LAYER: &[u8] = include_bytes!("fixtures/hlinks.tar.gz"); const TEST_REGISTRY_DEFAULT: &str = "localhost:5000"; @@ -514,7 +514,7 @@ async fn impl_test_container_import_export(chunked: bool) -> Result<()> { "/usr/bin/bash" ); - let n_chunks = if chunked { *CONTENTS_V0_LEN } else { 1 }; + let n_chunks = if chunked { LAYERS_V0_LEN } else { 1 }; assert_eq!(cfg.rootfs().diff_ids().len(), n_chunks); assert_eq!(cfg.history().len(), n_chunks); @@ -617,7 +617,7 @@ fn validate_chunked_structure(oci_path: &Utf8Path) -> Result<()> { let d = Dir::open_ambient_dir(oci_path, cap_std::ambient_authority())?; let d = ocidir::OciDir::open(&d)?; let manifest = d.read_manifest()?; - assert_eq!(manifest.layers().len(), *CONTENTS_V0_LEN); + assert_eq!(manifest.layers().len(), LAYERS_V0_LEN); let ostree_layer = manifest.layers().first().unwrap(); let mut ostree_layer_blob = d .read_blob(ostree_layer) @@ -650,7 +650,7 @@ fn validate_chunked_structure(oci_path: &Utf8Path) -> Result<()> { #[tokio::test] async fn test_container_chunked() -> Result<()> { - let nlayers = *CONTENTS_V0_LEN - 1; + let nlayers = LAYERS_V0_LEN - 1; let mut fixture = Fixture::new_v1()?; let (imgref, expected_digest) = fixture.export_container().await.unwrap(); @@ -717,8 +717,11 @@ r usr/bin/bash bash-v0 let (first, second) = (to_fetch[0], to_fetch[1]); assert!(first.0.commit.is_none()); assert!(second.0.commit.is_none()); - assert_eq!(first.1, "testlink"); - assert_eq!(second.1, "bash"); + assert_eq!( + first.1, + "ostree export of commit 38ab1f9da373a0184b0b48db6e280076ab4b5d4691773475ae24825aae2272d4" + ); + assert_eq!(second.1, "7 components"); assert_eq!(store::list_images(fixture.destrepo()).unwrap().len(), 1); let n = store::count_layer_references(fixture.destrepo())? as i64; @@ -792,7 +795,7 @@ r usr/bin/bash bash-v0 store::remove_images(fixture.destrepo(), [&derived_imgref.imgref]).unwrap(); assert_eq!(store::list_images(fixture.destrepo()).unwrap().len(), 0); let n_removed = store::gc_image_layers(fixture.destrepo())?; - assert_eq!(n_removed, (*CONTENTS_V0_LEN + 1) as u32); + assert_eq!(n_removed, (LAYERS_V0_LEN + 1) as u32); // Repo should be clean now assert_eq!(store::count_layer_references(fixture.destrepo())?, 0);