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
14 changes: 7 additions & 7 deletions crates/graphforge-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,13 @@ mod write_modes;

pub use graphforge_core::portable::{
PortableV2Authenticity, PortableV2Compatibility, PortableV2Error, PortableV2ErrorCode,
PortableV2ExportProgress, PortableV2GraphSelector, PortableV2GraphSubsetMeta,
PortableV2Integrity, PortableV2Limits, PortableV2Mode, PortableV2Output,
PortableV2PackageClass, PortableV2ParticipantId, PortableV2PropertyProjection,
PortableV2Representation, PortableV2SelectionEntry, PortableV2SelectionPlan,
PortableV2SelectionProfile, PortableV2SelectionReason, PortableV2SelectionRequest,
PortableV2SubsetClosure, PortableV2SubsetPreview as PortableV2SubsetPlan,
PortableV2SubsetRequest,
PortableV2ExactIdentity, PortableV2ExportProgress, PortableV2GraphSelector,
PortableV2GraphSubsetMeta, PortableV2Integrity, PortableV2Limits, PortableV2Mode,
PortableV2Output, PortableV2PackageClass, PortableV2ParticipantId,
PortableV2PropertyProjection, PortableV2Representation, PortableV2SelectionEntry,
PortableV2SelectionPlan, PortableV2SelectionProfile, PortableV2SelectionReason,
PortableV2SelectionRequest, PortableV2SubsetClosure,
PortableV2SubsetPreview as PortableV2SubsetPlan, PortableV2SubsetRequest,
};
pub use graphforge_core::storage_receipt::{
ArtifactCategory, ArtifactStorageTotals, StorageAttributionReceipt,
Expand Down
47 changes: 37 additions & 10 deletions crates/graphforge-cli/examples/generate_hub_fixture.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
//! Generate or check the deterministic Rust-owned Hub fixture artifacts.
//!
//! - no flag: check the checked-in artifacts for drift
//! - `--rebuild-source`: rebuild `openalex-source` through the public facade
//! - `--update`: regenerate `generated/v1` from the checked-in source
//!
//! The flags combine. After `--rebuild-source`, run `--update` because the
//! generated artifacts bind the source tree.

use graphforge_cli::hub_fixture_artifacts::{
DEFAULT_LOCATION_BASE, check, generate, rebuild_source,
};
use std::path::PathBuf;

fn regenerate(source: &std::path::Path, expected: &std::path::Path) -> Result<(), String> {
std::fs::remove_dir_all(expected)
.or_else(|error| {
(error.kind() == std::io::ErrorKind::NotFound)
.then_some(())
.ok_or(error)
})
.map_err(|error| error.to_string())
.and_then(|()| generate(source, expected, DEFAULT_LOCATION_BASE))
}

fn main() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let source = root.join("tests/fixtures/hub/openalex-source");
let expected = root.join("tests/fixtures/hub/generated/v1");
let result = if std::env::args().any(|argument| argument == "--update") {
std::fs::remove_dir_all(&expected)
.or_else(|error| {
(error.kind() == std::io::ErrorKind::NotFound)
.then_some(())
.ok_or(error)
})
.map_err(|error| error.to_string())
.and_then(|()| graphforge_cli::hub_fixture_artifacts::generate(&source, &expected))
let arguments: Vec<String> = std::env::args().collect();
let has = |flag: &str| arguments.iter().any(|argument| argument == flag);
let result = if has("--rebuild-source") || has("--update") {
let rebuilt = if has("--rebuild-source") {
rebuild_source(&source)
} else {
Ok(())
};
rebuilt.and_then(|()| {
if has("--update") {
regenerate(&source, &expected)
} else {
Ok(())
}
})
} else {
graphforge_cli::hub_fixture_artifacts::check(&source, &expected)
check(&source, &expected)
};
if let Err(error) = result {
eprintln!("{error}");
Expand Down
82 changes: 82 additions & 0 deletions crates/graphforge-cli/src/hub_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1951,6 +1951,88 @@ mod tests {
])
}

/// Scripted responses that also record every requested URL.
struct RecordingTransport {
inner: Scripted,
requested: Mutex<Vec<String>>,
}

impl Transport for RecordingTransport {
fn get(
&self,
url: &Url,
range: Option<u64>,
if_range: Option<&str>,
limit: u64,
) -> Result<HttpResponse, graphforge_api::GfError> {
self.requested.lock().unwrap().push(url.as_str().to_owned());
self.inner.get(url, range, if_range, limit)
}
}

#[test]
fn clone_succeeds_against_the_checked_in_hub_fixture() {
macro_rules! fixture {
($name:literal) => {
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/hub/generated/v1/",
$name
))
};
}
let refs = fixture!("refs.json");
let manifest_bytes = fixture!("manifest.json");
let project = fixture!("objects/openalex-openalex.gfpb");
let manifest = graphforge_discovery::DiscoveryManifest::from_json(
manifest_bytes,
graphforge_discovery::DiscoveryLimits::default(),
)
.unwrap();
// The fixture advertises a summary and a module package besides the
// Project package; clone needs neither and must not fetch them.
assert!(manifest.summary.is_some());
assert!(manifest.ontology.is_some());
assert!(manifest.objects.len() > 1);
let transport = RecordingTransport {
inner: Scripted::new(vec![
response(200, None, refs),
response(200, None, manifest_bytes),
response(200, None, project),
]),
requested: Mutex::new(Vec::new()),
};
let root = tempfile::tempdir().unwrap();
let destination = root.path().join("openalex");
let mut output = Vec::new();
run_clone_with(
&transport,
CloneArgs {
repository: "openalex/openalex".into(),
destination: Some(destination.clone()),
telemetry_endpoint: None,
},
true,
&mut output,
)
.unwrap();
assert_eq!(transport.inner.remaining(), 0);
let requested = transport.requested.lock().unwrap().clone();
assert_eq!(requested.len(), 3, "{requested:?}");
assert_eq!(
requested[2],
manifest.package_object().unwrap().locations[0],
"only the Project package object is downloaded"
);
let result: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(result["contract"], "graphforge-hub-clone/1");
assert_eq!(result["package_digest"], manifest.package.package_digest.0);
let cloned = GraphForge::new(destination.to_str()).expect("clone reopens");
let metadata = cloned.research_project_metadata().unwrap();
assert_eq!(metadata.title.as_deref(), Some("OpenAlex"));
assert_eq!(metadata.license.as_deref(), Some("CC0-1.0"));
}

#[test]
fn interrupted_clone_retains_resumed_and_transferred_bytes_once() {
let bundle = b"0123456789abcdef";
Expand Down
Loading