Skip to content

Commit 71ecafc

Browse files
authored
more versioned tests (#5)
Found a couple of bugs: 1. We were running git in the current directory rather than the repo root (whoops). 2. The symlink resolution for wire-compatible changes was slightly wrong.
1 parent 7eb6337 commit 71ecafc

10 files changed

Lines changed: 395 additions & 34 deletions

File tree

crates/dropshot-api-manager/src/cmd/check.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ pub(crate) fn check_impl(
3030
let (local_files, errors) = env.local_source.load(apis, &styles)?;
3131
display_load_problems(&errors, &styles)?;
3232

33-
let (blessed, errors) = blessed_source.load(apis, &styles)?;
33+
let (blessed, errors) =
34+
blessed_source.load(&env.repo_root, apis, &styles)?;
3435
display_load_problems(&errors, &styles)?;
3536

3637
let resolved = Resolved::new(env, apis, &blessed, &generated, &local_files);

crates/dropshot-api-manager/src/cmd/debug.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ pub(crate) fn debug_impl(
3030
dump_structure(&local_files, &errors);
3131

3232
// Print information about what we found in Git.
33-
let (blessed, errors) = blessed_source.load(apis, &styles)?;
33+
let (blessed, errors) =
34+
blessed_source.load(&env.repo_root, apis, &styles)?;
3435
dump_structure(&blessed, &errors);
3536

3637
// Print information about generated files.

crates/dropshot-api-manager/src/cmd/generate.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ pub(crate) fn generate_impl(
4949
let (local_files, errors) = env.local_source.load(apis, &styles)?;
5050
display_load_problems(&errors, &styles)?;
5151

52-
let (blessed, errors) = blessed_source.load(apis, &styles)?;
52+
let (blessed, errors) =
53+
blessed_source.load(&env.repo_root, apis, &styles)?;
5354
display_load_problems(&errors, &styles)?;
5455

5556
let resolved = Resolved::new(env, apis, &blessed, &generated, &local_files);

crates/dropshot-api-manager/src/environment.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ impl BlessedSource {
202202
/// Load the blessed OpenAPI documents
203203
pub fn load(
204204
&self,
205+
repo_root: &Utf8Path,
205206
apis: &ManagedApis,
206207
styles: &Styles,
207208
) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
@@ -227,6 +228,7 @@ impl BlessedSource {
227228
);
228229
Ok((
229230
BlessedFiles::load_from_git_parent_branch(
231+
repo_root,
230232
revision,
231233
directory,
232234
apis,

crates/dropshot-api-manager/src/git.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ NewtypeFrom! { () pub struct GitRevision(String); }
2020

2121
/// Given a revision, return its merge base with HEAD
2222
pub fn git_merge_base_head(
23+
repo_root: &Utf8Path,
2324
revision: &GitRevision,
2425
) -> anyhow::Result<GitRevision> {
25-
let mut cmd = git_start();
26+
let mut cmd = git_start(repo_root);
2627
cmd.arg("merge-base").arg("--all").arg("HEAD").arg(revision.as_str());
2728
let label = cmd_label(&cmd);
2829
let stdout = do_run(&mut cmd)?;
@@ -39,10 +40,11 @@ pub fn git_merge_base_head(
3940

4041
/// List files recursively under some path `path` in Git revision `revision`.
4142
pub fn git_ls_tree(
43+
repo_root: &Utf8Path,
4244
revision: &GitRevision,
4345
directory: &Utf8Path,
4446
) -> anyhow::Result<Vec<Utf8PathBuf>> {
45-
let mut cmd = git_start();
47+
let mut cmd = git_start(repo_root);
4648
cmd.arg("ls-tree")
4749
.arg("-r")
4850
.arg("-z")
@@ -75,19 +77,22 @@ pub fn git_ls_tree(
7577
/// Returns the contents of the file at the given path `path` in Git revision
7678
/// `revision`.
7779
pub fn git_show_file(
80+
repo_root: &Utf8Path,
7881
revision: &GitRevision,
7982
path: &Utf8Path,
8083
) -> anyhow::Result<Vec<u8>> {
81-
let mut cmd = git_start();
84+
let mut cmd = git_start(repo_root);
8285
cmd.arg("cat-file").arg("blob").arg(format!("{}:{}", revision, path));
8386
let stdout = do_run(&mut cmd)?;
8487
Ok(stdout.into_bytes())
8588
}
8689

8790
/// Begin assembling an invocation of git(1)
88-
fn git_start() -> Command {
91+
fn git_start(repo_root: &Utf8Path) -> Command {
8992
let git = std::env::var("GIT").ok().unwrap_or_else(|| String::from("git"));
90-
Command::new(&git)
93+
let mut command = Command::new(&git);
94+
command.current_dir(repo_root);
95+
command
9196
}
9297

9398
/// Runs an assembled git(1) command, returning stdout on success and an error

crates/dropshot-api-manager/src/resolved.rs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -679,37 +679,82 @@ fn resolve_api<'a>(
679679
} else {
680680
// latest_local is different from latest_generated.
681681
//
682-
// We never want to update blessed documents. But
683-
// latest_generated might have wire-compatible changes which
684-
// would cause the hash to change.
682+
// We never want to update the local copies of blessed
683+
// documents. But latest_generated might have
684+
// wire-compatible (trivial) changes which would cause the
685+
// hash to change, so we need to handle this case with care.
685686
//
686-
// There are two possibilities:
687+
// The possibilities are:
687688
//
688-
// 1. latest_local is blessed and latest_generated has
689-
// wire-compatible changes. In that case, we don't update
690-
// the symlink.
691-
// 2. latest_local is blessed and latest_generated has
689+
// 1. latest_local is blessed, latest_generated has the same
690+
// version as latest_local, and it has wire-compatible
691+
// changes. In that case, don't update the symlink.
692+
// 2. latest_local is blessed, latest_generated has the same
693+
// version as latest_local, and latest_generated has
692694
// wire-*incompatible* changes. In that case, we'd have
693695
// returned errors in the by_version map above, and we
694696
// wouldn't want to update the symlink in any case.
695-
// 3. latest_local is not blessed. In that case, we do
697+
// 3. latest_local is blessed, and latest_generated is
698+
// blessed but a *different* version. This means that
699+
// the latest version was retired. In this case,
700+
// we want to update the symlink to the blessed hash
701+
// corresponding to the latest generated version.
702+
// 4. latest_local is not blessed. In that case, we do
696703
// want to update the symlink.
697-
//
698-
// This boils down to simply checking if latest_generated is
699-
// a blessed version.
700-
let version = latest_generated
704+
let generated_version = latest_generated
705+
.version()
706+
.expect("versioned APIs have a version");
707+
let local_version = latest_local
701708
.version()
702709
.expect("versioned APIs have a version");
703-
if let Some(resolution) = by_version.get(version) {
710+
if let Some(resolution) = by_version.get(generated_version)
711+
{
704712
match resolution.kind() {
705713
ResolutionKind::Lockstep => {
706714
unreachable!("this is a versioned API");
707715
}
708-
ResolutionKind::Blessed => {
709-
// latest_generated is blessed, so don't update
710-
// the symlink.
716+
// Case 1 and 2 above.
717+
ResolutionKind::Blessed
718+
if generated_version == local_version =>
719+
{
720+
// latest_generated is blessed and the same
721+
// version as latest_local, so don't update the
722+
// symlink.
711723
None
712724
}
725+
ResolutionKind::Blessed => {
726+
// latest_generated is blessed, and has a
727+
// different version from latest_local. In this
728+
// case, we want to update the symlink to the
729+
// blessed version matching latest_generated
730+
// (not latest_generated, in case it's different
731+
// from the blessed version in a wire-compatible
732+
// way!)
733+
let api_blessed =
734+
api_blessed.unwrap_or_else(|| {
735+
panic!(
736+
"for {}, Blessed means \
737+
api_blessed exists",
738+
api.ident()
739+
)
740+
});
741+
let blessed = api_blessed
742+
.versions()
743+
.get(generated_version)
744+
.unwrap_or_else(|| {
745+
panic!(
746+
"for {} v{}, Blessed means \
747+
generated_version exists",
748+
api.ident(),
749+
generated_version
750+
);
751+
});
752+
Some(Problem::LatestLinkStale {
753+
api_ident: api.ident().clone(),
754+
link: blessed.spec_file_name(),
755+
found: latest_local,
756+
})
757+
}
713758
ResolutionKind::NewLocally => {
714759
// latest_generated is not blessed, so update
715760
// the symlink.

crates/dropshot-api-manager/src/spec_files_blessed.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,15 @@ impl BlessedFiles {
9595
/// `M1` for blessed documents because you haven't yet merged in commits M2,
9696
/// M3, and M4.
9797
pub fn load_from_git_parent_branch(
98+
repo_root: &Utf8Path,
9899
branch: &GitRevision,
99100
directory: &Utf8Path,
100101
apis: &ManagedApis,
101102
error_accumulator: &mut ErrorAccumulator,
102103
) -> anyhow::Result<BlessedFiles> {
103-
let revision = git_merge_base_head(branch)?;
104+
let revision = git_merge_base_head(repo_root, branch)?;
104105
Self::load_from_git_revision(
106+
repo_root,
105107
&revision,
106108
directory,
107109
apis,
@@ -111,14 +113,15 @@ impl BlessedFiles {
111113

112114
/// Load OpenAPI documents from the given Git revision and directory.
113115
pub fn load_from_git_revision(
116+
repo_root: &Utf8Path,
114117
commit: &GitRevision,
115118
directory: &Utf8Path,
116119
apis: &ManagedApis,
117120
error_accumulator: &mut ErrorAccumulator,
118121
) -> anyhow::Result<BlessedFiles> {
119122
let mut api_files: ApiSpecFilesBuilder<BlessedApiSpecFile> =
120123
ApiSpecFilesBuilder::new(apis, error_accumulator);
121-
let files_found = git_ls_tree(commit, directory)?;
124+
let files_found = git_ls_tree(repo_root, commit, directory)?;
122125
for f in files_found {
123126
// We should be looking at either a single-component path
124127
// ("api.json") or a file inside one level of directory hierarchy
@@ -134,7 +137,8 @@ impl BlessedFiles {
134137

135138
// Read the contents. Use "/" rather than "\" on Windows.
136139
let file_name = format!("{directory}/{f}");
137-
let contents = git_show_file(commit, file_name.as_ref())?;
140+
let contents =
141+
git_show_file(repo_root, commit, file_name.as_ref())?;
138142
if parts.len() == 1 {
139143
if let Some(file_name) = api_files.lockstep_file_name(parts[0])
140144
{

crates/integration-tests/src/common/fixtures.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,43 @@ pub mod versioned_user {
451451
pub permissions: Vec<String>,
452452
}
453453
}
454+
455+
/// Reduced versioned health API for testing version removal scenarios.
456+
pub mod versioned_health_reduced {
457+
use super::*;
458+
use dropshot_api_manager_types::api_versions;
459+
460+
api_versions!([(2, WITH_DETAILED_STATUS), (1, INITIAL),]);
461+
462+
#[dropshot::api_description]
463+
pub trait VersionedHealthApi {
464+
type Context;
465+
466+
/// Check if the service is healthy (all versions).
467+
#[endpoint {
468+
method = GET,
469+
path = "/health",
470+
operation_id = "health_check",
471+
versions = "1.0.0"..
472+
}]
473+
async fn health_check(
474+
rqctx: RequestContext<Self::Context>,
475+
) -> Result<HttpResponseOk<HealthStatusV1>, HttpError>;
476+
477+
/// Get detailed health status (v2+).
478+
#[endpoint {
479+
method = GET,
480+
path = "/health/detailed",
481+
operation_id = "detailed_health_check",
482+
versions = "2.0.0"..
483+
}]
484+
async fn detailed_health_check(
485+
rqctx: RequestContext<Self::Context>,
486+
) -> Result<HttpResponseOk<DetailedHealthStatus>, HttpError>;
487+
}
488+
489+
// Reuse the same response types from the main versioned_health module.
490+
pub use super::versioned_health::{
491+
DependencyStatus, DetailedHealthStatus, HealthStatusV1,
492+
};
493+
}

crates/integration-tests/src/common/mod.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ impl TestEnvironment {
4343
let documents_dir = workspace_root.child("documents");
4444

4545
// Initialize git repository in workspace root.
46-
Self::run_git_command(&workspace_root, &["init"])?;
46+
Self::run_git_command(
47+
&workspace_root,
48+
&["init", "--initial-branch", "main"],
49+
)?;
4750
Self::run_git_command(
4851
&workspace_root,
4952
&["config", "user.name", "Test User"],
@@ -70,7 +73,11 @@ impl TestEnvironment {
7073
"test-openapi-manager",
7174
workspace_root.as_path(),
7275
"documents",
73-
)?;
76+
)?
77+
// Use "main" rather than the default "origin/main" since we're not
78+
// pushing to an upstream. A commit to main automatically marks the
79+
// document blessed.
80+
.with_default_git_branch("main");
7481

7582
Ok(Self { temp_dir, workspace_root, documents_dir, environment })
7683
}
@@ -110,6 +117,15 @@ impl TestEnvironment {
110117
.with_context(|| format!("failed to read file: {}", path))
111118
}
112119

120+
pub fn read_link(
121+
&self,
122+
relative_path: impl AsRef<Utf8Path>,
123+
) -> Result<Utf8PathBuf> {
124+
let path = self.workspace_root.join(relative_path);
125+
path.read_link_utf8()
126+
.with_context(|| format!("failed to read link: {}", path))
127+
}
128+
113129
/// Check if a file exists in the workspace.
114130
pub fn file_exists(&self, relative_path: impl AsRef<Utf8Path>) -> bool {
115131
self.workspace_root.join(relative_path.as_ref()).exists()
@@ -199,10 +215,13 @@ impl TestEnvironment {
199215
&self,
200216
api_ident: &str,
201217
) -> Result<String> {
202-
self.read_file(format!(
203-
"documents/{}/{}-latest.json",
204-
api_ident, api_ident
205-
))
218+
// Try reading the link to ensure it's a symlink.
219+
let file_name =
220+
format!("documents/{}/{}-latest.json", api_ident, api_ident);
221+
let target = self.read_link(&file_name)?;
222+
eprintln!("** symlink target: {}", target);
223+
224+
self.read_file(&file_name)
206225
}
207226

208227
/// Commit documents to git (for blessed document workflow testing).
@@ -456,3 +475,42 @@ pub fn create_mixed_test_apis() -> Result<ManagedApis> {
456475
];
457476
ManagedApis::new(configs).context("failed to create mixed ManagedApis")
458477
}
478+
479+
/// Create versioned health API with a trivial change (title/metadata updated).
480+
pub fn create_versioned_health_test_apis_with_trivial_change()
481+
-> Result<ManagedApis> {
482+
// Create a modified API config that would produce different OpenAPI documents.
483+
let mut config = versioned_health_test_api();
484+
485+
// Modify the title to create a different document signature.
486+
config.title = "Modified Versioned Health API";
487+
config.metadata.description =
488+
Some("A versioned health API with breaking changes");
489+
490+
ManagedApis::new(vec![config])
491+
.context("failed to create trivial change versioned health ManagedApis")
492+
}
493+
494+
/// Create versioned health API with reduced versions (simulating version removal).
495+
pub fn create_versioned_health_test_apis_reduced_versions()
496+
-> Result<ManagedApis> {
497+
// Create a configuration similar to versioned health but with fewer versions.
498+
// We'll create a new fixture for this.
499+
let config = ManagedApiConfig {
500+
ident: "versioned-health",
501+
versions: Versions::Versioned {
502+
// Use a subset of versions (only 1.0.0 and 2.0.0, not 3.0.0).
503+
supported_versions: fixtures::versioned_health_reduced::supported_versions(),
504+
},
505+
title: "Versioned Health API",
506+
metadata: ManagedApiMetadata {
507+
description: Some("A versioned health API with reduced versions"),
508+
..Default::default()
509+
},
510+
api_description: fixtures::versioned_health_reduced::versioned_health_api_mod::stub_api_description,
511+
extra_validation: None,
512+
};
513+
514+
ManagedApis::new(vec![config])
515+
.context("failed to create reduced versioned health ManagedApis")
516+
}

0 commit comments

Comments
 (0)