Skip to content
Draft
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
42 changes: 21 additions & 21 deletions atcoder-problems-backend/src/crawler_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,29 +244,29 @@ async fn upsert_problems(
new_problems: Vec<Problem>,
) -> Result<usize, DbErr> {
for problem in &new_problems {
// Insert into problems table
let title = problem.title();
let model = sql_entities::problems::ActiveModel {
id: Set(problem.id.clone()),
contest_id: Set(problem.contest_id.clone()),
problem_index: Set(problem.problem_index.clone()),
name: Set(problem.name.clone()),
title: Set(title),
};
sql_entities::problems::Entity::insert(model)
.on_conflict(
OnConflict::column(sql_entities::problems::Column::Id)
.update_columns([
sql_entities::problems::Column::ContestId,
sql_entities::problems::Column::ProblemIndex,
sql_entities::problems::Column::Name,
sql_entities::problems::Column::Title,
])
.to_owned(),
)
.exec(db)
// Check if problem already exists; only insert if it doesn't.
// This prevents overwriting existing problem metadata (e.g. original contest_id)
// when the same problem_id appears in multiple contests (ADT reuses ABC problems).
// In other words: this is an ADT-specific countermeasure to keep the first-seen
// contest as the canonical one for the problem.
let existing = sql_entities::problems::Entity::find_by_id(problem.id.clone())
.one(db)
.await?;

if existing.is_none() {
let title = problem.title();
let model = sql_entities::problems::ActiveModel {
id: Set(problem.id.clone()),
contest_id: Set(problem.contest_id.clone()),
problem_index: Set(problem.problem_index.clone()),
name: Set(problem.name.clone()),
title: Set(title),
};
sql_entities::problems::Entity::insert(model)
.exec(db)
.await?;
}

// Insert into contest_problem table
let contest_problem = sql_entities::contest_problem::ActiveModel {
contest_id: Set(problem.contest_id.clone()),
Expand Down
90 changes: 90 additions & 0 deletions atcoder-problems-backend/tests/test_crawler_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,93 @@ async fn test_crawl_contests_fetches_filtered_archive_categories() {
.any(|contest| contest.id == "adt_all_20260612_2")
);
}

#[tokio::test]
async fn test_crawl_adt_problems() {
let db = setup_db().await.unwrap();

// 1. ABCのコンテスト、問題がクロールされる
sql_entities::contests::Entity::insert(sql_entities::contests::ActiveModel {
id: Set("abc001".to_string()),
start_epoch_second: Set(0),
duration_second: Set(0),
title: Set("AtCoder Beginner Contest 001".to_string()),
rate_change: Set("?".to_string()),
})
.exec(&db)
.await
.unwrap();

let mut abc_fetcher = MockProblemFetcher::new();
abc_fetcher
.expect_fetch_problems()
.withf(|contest_id| contest_id == "abc001")
.times(1)
.returning(|_| {
Ok(vec![Problem {
id: "abc001_a".to_string(),
contest_id: "abc001".to_string(),
problem_index: "A".to_string(),
name: "Problem A".to_string(),
}])
});

atcoder_problems_backend::crawler_utils::crawl_problems(&abc_fetcher, &db)
.await
.unwrap();

// 2. 次に、ADTのコンテスト、問題がクロールされる
sql_entities::contests::Entity::insert(sql_entities::contests::ActiveModel {
id: Set("adt_all_20260612".to_string()),
start_epoch_second: Set(0),
duration_second: Set(0),
title: Set("AtCoder Daily Training 2026/06/12 All".to_string()),
rate_change: Set("-".to_string()),
})
.exec(&db)
.await
.unwrap();

let mut adt_fetcher = MockProblemFetcher::new();
adt_fetcher
.expect_fetch_problems()
.withf(|contest_id| contest_id == "adt_all_20260612")
.times(1)
.returning(|_| {
Ok(vec![Problem {
id: "abc001_a".to_string(),
contest_id: "adt_all_20260612".to_string(),
problem_index: "C".to_string(),
name: "Problem C".to_string(),
}])
});

atcoder_problems_backend::crawler_utils::crawl_problems(&adt_fetcher, &db)
.await
.unwrap();

// 3. abc001_aがabc001とadt_all_20260612の両方に紐づいていることを確認する
let problems_after_adt = sql_entities::problems::Entity::find()
.all(&db)
.await
.unwrap();
assert_eq!(problems_after_adt.len(), 1);
assert_eq!(problems_after_adt[0].id, "abc001_a");
assert_eq!(problems_after_adt[0].contest_id, "abc001");

let contest_problems = sql_entities::contest_problem::Entity::find()
.all(&db)
.await
.unwrap();
assert_eq!(contest_problems.len(), 2);
assert!(contest_problems.iter().any(|cp| cp.contest_id == "abc001"
&& cp.problem_id == "abc001_a"
&& cp.problem_index == "A"));
assert!(
contest_problems
.iter()
.any(|cp| cp.contest_id == "adt_all_20260612"
&& cp.problem_id == "abc001_a"
&& cp.problem_index == "C")
);
}
Loading