feat: persist diff file review checkmarks
This commit is contained in:
+244
-6
@@ -38,6 +38,16 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
|
||||
body: "Please preserve 👩🔬".into(),
|
||||
resolved: true,
|
||||
});
|
||||
review.reviewed_files = [Some("live 👩🔬\r\n"), None, Some("")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, after)| ReviewedFile {
|
||||
revisions: [format!("before-{index}"), "after".into()],
|
||||
path: "日本語.rs".into(),
|
||||
old_path: (index == 0).then(|| "old.rs".into()),
|
||||
after: after.map(str::to_owned),
|
||||
})
|
||||
.collect();
|
||||
{
|
||||
let mut store = Store::open(&storage).unwrap();
|
||||
store.save(&mut review).unwrap();
|
||||
@@ -50,10 +60,12 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
|
||||
assert_eq!(listed[0].storage_id, Some(id));
|
||||
assert_eq!(listed[0].summary, review.summary);
|
||||
assert_eq!(listed[0].comments, review.comments);
|
||||
assert_eq!(listed[0].reviewed_files, review.reviewed_files);
|
||||
let mut loaded = store.load(&root, id).unwrap();
|
||||
assert_eq!(loaded.storage_id, Some(id));
|
||||
assert_eq!(loaded.storage_revision, 1);
|
||||
assert_eq!(loaded.comments, review.comments);
|
||||
assert_eq!(loaded.reviewed_files, review.reviewed_files);
|
||||
assert_eq!(loaded.verdict, review.verdict);
|
||||
assert!(!loaded.comments[0].anchor.matches_lines(
|
||||
"working:new-snapshot",
|
||||
@@ -67,7 +79,10 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
|
||||
let exported = fs::read(&path).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&exported).unwrap();
|
||||
assert_eq!(json["comments"][0]["resolved"], true);
|
||||
assert_eq!(json["version"], 3);
|
||||
assert_eq!(json["version"], 4);
|
||||
assert_eq!(json["reviewed_files"][0]["after"], "live 👩🔬\r\n");
|
||||
assert!(json["reviewed_files"][1]["after"].is_null());
|
||||
assert_eq!(json["reviewed_files"][2]["after"], "");
|
||||
assert!(json.get("storage_id").is_none());
|
||||
assert!(json.get("storage_revision").is_none());
|
||||
loaded.summary = "Database only".into();
|
||||
@@ -78,9 +93,28 @@ fn database_restarts_preserve_identity_and_exact_stale_anchors() {
|
||||
let mut imported = Review::load(&path, &root).unwrap();
|
||||
assert_eq!(imported.storage_id, None);
|
||||
assert_eq!(imported.storage_revision, 0);
|
||||
assert_eq!(imported.reviewed_files, review.reviewed_files);
|
||||
store.save(&mut imported).unwrap();
|
||||
assert_ne!(imported.storage_id, Some(id));
|
||||
assert_eq!(store.list(&root).unwrap().len(), 2);
|
||||
assert_eq!(
|
||||
store
|
||||
.load(&root, imported.storage_id.unwrap())
|
||||
.unwrap()
|
||||
.reviewed_files,
|
||||
review.reviewed_files,
|
||||
);
|
||||
let comments = loaded.comments.clone();
|
||||
let summary = loaded.summary.clone();
|
||||
let verdict = loaded.verdict;
|
||||
loaded.reviewed_files.clear();
|
||||
store.save(&mut loaded).unwrap();
|
||||
drop(store);
|
||||
let loaded = Store::open(&storage).unwrap().load(&root, id).unwrap();
|
||||
assert!(loaded.reviewed_files.is_empty());
|
||||
assert_eq!(loaded.comments, comments);
|
||||
assert_eq!(loaded.summary, summary);
|
||||
assert_eq!(loaded.verdict, verdict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,12 +156,20 @@ fn database_conflicts_and_locked_writes_keep_drafts_and_identity() {
|
||||
let mut first = Store::open(&storage).unwrap();
|
||||
let mut second = Store::open(&storage).unwrap();
|
||||
let mut review = Review::new(root.clone());
|
||||
review.reviewed_files.push(ReviewedFile {
|
||||
revisions: ["before".into(), "after".into()],
|
||||
path: "source.rs".into(),
|
||||
old_path: None,
|
||||
after: None,
|
||||
});
|
||||
first.save(&mut review).unwrap();
|
||||
let id = review.storage_id.unwrap();
|
||||
let mut draft = second.load(&root, id).unwrap();
|
||||
review.summary = "First writer".into();
|
||||
review.reviewed_files[0].after = Some("First writer buffer\n".into());
|
||||
first.save(&mut review).unwrap();
|
||||
draft.summary = "Second writer draft".into();
|
||||
draft.reviewed_files.clear();
|
||||
assert!(
|
||||
second
|
||||
.save(&mut draft)
|
||||
@@ -137,6 +179,11 @@ fn database_conflicts_and_locked_writes_keep_drafts_and_identity() {
|
||||
assert_eq!(draft.summary, "Second writer draft");
|
||||
assert_eq!((draft.storage_id, draft.storage_revision), (Some(id), 1));
|
||||
assert_eq!(second.load(&root, id).unwrap().summary, "First writer");
|
||||
assert_eq!(
|
||||
second.load(&root, id).unwrap().reviewed_files,
|
||||
review.reviewed_files,
|
||||
);
|
||||
assert!(draft.reviewed_files.is_empty());
|
||||
first.connection.execute_batch("BEGIN IMMEDIATE").unwrap();
|
||||
let mut fresh = Review::new(root.clone());
|
||||
assert!(second.save(&mut fresh).unwrap_err().contains("locked"));
|
||||
@@ -148,7 +195,13 @@ fn database_conflicts_and_locked_writes_keep_drafts_and_identity() {
|
||||
|
||||
#[test]
|
||||
fn incompatible_database_schemas_are_preserved() {
|
||||
let previous = format!(
|
||||
"{REVIEWS_SCHEMA}; {COMMENTS_SCHEMA};
|
||||
INSERT INTO reviews VALUES (1, '/old/repo', 1, 'Preserve me', 'looks_good');
|
||||
PRAGMA user_version = 2"
|
||||
);
|
||||
for schema in [
|
||||
previous.as_str(),
|
||||
"CREATE TABLE other (value TEXT); PRAGMA user_version = 42",
|
||||
"CREATE TABLE other (value TEXT); PRAGMA user_version = 1",
|
||||
"CREATE TABLE reviews (id INTEGER PRIMARY KEY, root TEXT NOT NULL,
|
||||
@@ -163,6 +216,8 @@ fn incompatible_database_schemas_are_preserved() {
|
||||
connection.execute_batch(schema).unwrap();
|
||||
drop(connection);
|
||||
let original = fs::read(&path).unwrap();
|
||||
assert!(Store::open_read_only(&directory.0).is_err());
|
||||
assert_eq!(fs::read(&path).unwrap(), original);
|
||||
assert!(Store::open(&directory.0).err().unwrap().contains("schema"));
|
||||
assert_eq!(fs::read(&path).unwrap(), original);
|
||||
}
|
||||
@@ -461,6 +516,12 @@ fn comment_rows_preserve_order_full_ids_and_atomic_replacement() {
|
||||
})
|
||||
.collect();
|
||||
review.summary = "Original".into();
|
||||
review.reviewed_files.push(ReviewedFile {
|
||||
revisions: ["before".into(), "after".into()],
|
||||
path: "source.rs".into(),
|
||||
old_path: None,
|
||||
after: None,
|
||||
});
|
||||
store.save(&mut review).unwrap();
|
||||
let id = review.storage_id.unwrap();
|
||||
assert_eq!(store.load(&root, id).unwrap().comments, review.comments);
|
||||
@@ -488,6 +549,7 @@ fn comment_rows_preserve_order_full_ids_and_atomic_replacement() {
|
||||
.unwrap();
|
||||
let revision = review.storage_revision;
|
||||
let original = review.comments.clone();
|
||||
let original_files = review.reviewed_files.clone();
|
||||
review.summary = "Unsaved".into();
|
||||
review.comments[0].body = "Changed".into();
|
||||
assert!(
|
||||
@@ -501,10 +563,30 @@ fn comment_rows_preserve_order_full_ids_and_atomic_replacement() {
|
||||
assert_eq!(loaded.summary, "Original");
|
||||
assert_eq!(loaded.comments, original);
|
||||
assert_eq!(loaded.storage_revision, revision);
|
||||
assert_eq!(loaded.reviewed_files, original_files);
|
||||
store
|
||||
.connection
|
||||
.execute_batch("DROP TRIGGER reject_comment")
|
||||
.unwrap();
|
||||
store
|
||||
.connection
|
||||
.execute_batch(
|
||||
"CREATE TEMP TRIGGER reject_reviewed BEFORE INSERT ON reviewed_files
|
||||
BEGIN SELECT RAISE(ABORT, 'blocked reviewed file'); END",
|
||||
)
|
||||
.unwrap();
|
||||
review.reviewed_files[0].after = Some("Unsaved buffer\n".into());
|
||||
assert!(store.save(&mut review).is_err());
|
||||
assert_eq!(review.storage_revision, revision);
|
||||
let loaded = store.load(&root, id).unwrap();
|
||||
assert_eq!(loaded.summary, "Original");
|
||||
assert_eq!(loaded.comments, original);
|
||||
assert_eq!(loaded.reviewed_files, original_files);
|
||||
assert_eq!(loaded.storage_revision, revision);
|
||||
store
|
||||
.connection
|
||||
.execute_batch("DROP TRIGGER reject_reviewed")
|
||||
.unwrap();
|
||||
review.comments.reverse();
|
||||
store.save(&mut review).unwrap();
|
||||
assert_eq!(store.load(&root, id).unwrap().comments, review.comments);
|
||||
@@ -547,16 +629,16 @@ fn exact_grapheme_ranges_roundtrip_and_reject_invalid_coordinates() {
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("resolved");
|
||||
for version in [2, 3] {
|
||||
for version in [2, 3, 4] {
|
||||
missing_resolution["version"] = version.into();
|
||||
let bytes = serde_json::to_vec(&missing_resolution).unwrap();
|
||||
fs::write(&path, &bytes).unwrap();
|
||||
let error = Review::load(&path, &root).unwrap_err();
|
||||
assert!(error.contains(if version == 2 {
|
||||
"Unsupported review version 2"
|
||||
if version < 4 {
|
||||
assert!(error.contains("Unsupported review version"));
|
||||
} else {
|
||||
"missing field `resolved`"
|
||||
}));
|
||||
assert!(error.contains("missing field"));
|
||||
}
|
||||
assert_eq!(fs::read(&path).unwrap(), bytes);
|
||||
}
|
||||
for (field, value) in [
|
||||
@@ -584,3 +666,159 @@ fn exact_grapheme_ranges_roundtrip_and_reject_invalid_coordinates() {
|
||||
loaded.comments[0].anchor.end = TextPosition { row: 0, column: 0 };
|
||||
assert!(loaded.save(&root.join("backwards.json")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_reject_invalid_json_and_preserve_existing_data() {
|
||||
let directory = directory();
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
let path = root.join("review.json");
|
||||
let mut store = Store::open(&root.join("storage")).unwrap();
|
||||
let mut review = Review::new(root.clone());
|
||||
review.reviewed_files.push(ReviewedFile {
|
||||
revisions: ["before".into(), "after".into()],
|
||||
path: "source.rs".into(),
|
||||
old_path: Some("old.rs".into()),
|
||||
after: Some("Exact live text\r\n".into()),
|
||||
});
|
||||
store.save(&mut review).unwrap();
|
||||
let valid = serde_json::to_value(&review).unwrap();
|
||||
let mut invalid_documents = Vec::new();
|
||||
for (field, value) in [
|
||||
("path", serde_json::json!("")),
|
||||
("path", serde_json::json!("../outside")),
|
||||
("path", serde_json::json!("/absolute")),
|
||||
("path", serde_json::json!(".git/config")),
|
||||
("path", serde_json::json!("nul\0.rs")),
|
||||
("old_path", serde_json::json!("../outside")),
|
||||
("old_path", serde_json::json!("/absolute")),
|
||||
("old_path", serde_json::json!(".jj/repo")),
|
||||
("old_path", serde_json::json!("nul\0.rs")),
|
||||
("revisions", serde_json::json!(["", "after"])),
|
||||
("revisions", serde_json::json!(["before", ""])),
|
||||
("revisions", serde_json::json!(["before\n", "after"])),
|
||||
("revisions", serde_json::json!(["before", "after\0"])),
|
||||
("revisions", serde_json::json!(["before"])),
|
||||
("unexpected", serde_json::json!(true)),
|
||||
] {
|
||||
let mut invalid = valid.clone();
|
||||
invalid["reviewed_files"][0][field] = value;
|
||||
invalid_documents.push(invalid);
|
||||
}
|
||||
let mut duplicate = valid.clone();
|
||||
duplicate["reviewed_files"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(valid["reviewed_files"][0].clone());
|
||||
invalid_documents.push(duplicate);
|
||||
let mut missing = valid.clone();
|
||||
missing.as_object_mut().unwrap().remove("reviewed_files");
|
||||
invalid_documents.push(missing);
|
||||
let mut old = valid;
|
||||
old["version"] = 3.into();
|
||||
invalid_documents.push(old);
|
||||
for invalid in invalid_documents {
|
||||
let bytes = serde_json::to_vec(&invalid).unwrap();
|
||||
fs::write(&path, &bytes).unwrap();
|
||||
assert!(Review::load(&path, &root).is_err());
|
||||
assert_eq!(fs::read(&path).unwrap(), bytes);
|
||||
if let Ok(mut invalid) = serde_json::from_value::<Review>(invalid) {
|
||||
assert!(store.save(&mut invalid).is_err());
|
||||
assert!(invalid.save(&root.join("invalid.json")).is_err());
|
||||
assert!(!root.join("invalid.json").exists());
|
||||
}
|
||||
}
|
||||
let loaded = store.load(&root, review.storage_id.unwrap()).unwrap();
|
||||
assert_eq!(loaded.reviewed_files, review.reviewed_files);
|
||||
assert_eq!(store.list(&root).unwrap().len(), 1);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
for old_path in [false, true] {
|
||||
let mut invalid = review.clone();
|
||||
let path = PathBuf::from(std::ffi::OsString::from_vec(vec![0xff]));
|
||||
if old_path {
|
||||
invalid.reviewed_files[0].old_path = Some(path);
|
||||
} else {
|
||||
invalid.reviewed_files[0].path = path;
|
||||
}
|
||||
assert!(store.save(&mut invalid).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_file_rows_enforce_identity_and_limit_snapshots_before_loading() {
|
||||
let directory = directory();
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
let mut store = Store::open(&root.join("storage")).unwrap();
|
||||
let mut review = Review::new(root.clone());
|
||||
review.reviewed_files.push(ReviewedFile {
|
||||
revisions: ["before".into(), "after".into()],
|
||||
path: "source.rs".into(),
|
||||
old_path: None,
|
||||
after: None,
|
||||
});
|
||||
store.save(&mut review).unwrap();
|
||||
let id = review.storage_id.unwrap();
|
||||
for update in [
|
||||
"review_id = 0",
|
||||
"before_revision = ''",
|
||||
"after_revision = ''",
|
||||
"path = NULL",
|
||||
"after_content = zeroblob(1)",
|
||||
"position = -1",
|
||||
] {
|
||||
assert!(
|
||||
store
|
||||
.connection
|
||||
.execute(
|
||||
&format!("UPDATE reviewed_files SET {update} WHERE review_id = ?1"),
|
||||
[id],
|
||||
)
|
||||
.is_err(),
|
||||
"{update}",
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
store
|
||||
.connection
|
||||
.execute(
|
||||
"INSERT INTO reviewed_files
|
||||
SELECT review_id, position + 1, before_revision, after_revision,
|
||||
path, old_path, after_content FROM reviewed_files WHERE review_id = ?1",
|
||||
[id],
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
for update in [
|
||||
"before_revision = char(10)",
|
||||
"after_revision = 'after' || char(0)",
|
||||
"path = '../outside'",
|
||||
"old_path = '../outside'",
|
||||
"old_path = 'nul' || char(0)",
|
||||
"position = 1",
|
||||
] {
|
||||
store
|
||||
.connection
|
||||
.execute(
|
||||
&format!("UPDATE reviewed_files SET {update} WHERE review_id = ?1"),
|
||||
[id],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(store.load(&root, id).is_err(), "{update}");
|
||||
assert!(store.list(&root).is_err(), "{update}");
|
||||
store.save(&mut review).unwrap();
|
||||
}
|
||||
store
|
||||
.connection
|
||||
.execute(
|
||||
"UPDATE reviewed_files SET after_content = CAST(zeroblob(?1) AS TEXT)
|
||||
WHERE review_id = ?2",
|
||||
params![LIMIT as i64 + 1, id],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(store.load(&root, id).unwrap_err().contains("64 MiB"));
|
||||
assert!(store.list(&root).unwrap_err().contains("64 MiB"));
|
||||
review.reviewed_files[0].after = Some("x".repeat(LIMIT));
|
||||
assert!(store.save(&mut review).unwrap_err().contains("64 MiB"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user