feat: rebuild CSP practice workflow, UX and automation

这个提交包含在:
Codex CLI
2026-02-13 15:49:05 +08:00
父节点 d33deed4c5
当前提交 e2ab522b78
修改 105 个文件,包含 15669 行新增428 行删除

查看文件

@@ -0,0 +1,94 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/contest_controller.h"
#include "csp/services/auth_service.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallList(csp::controllers::ContestController& ctl) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.list(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallGet(csp::controllers::ContestController& ctl,
int64_t contest_id,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.getById(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
contest_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallRegister(csp::controllers::ContestController& ctl,
int64_t contest_id,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Post);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.registerForContest(req,
[&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
},
contest_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallBoard(csp::controllers::ContestController& ctl,
int64_t contest_id) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.leaderboard(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
contest_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("contest controller list/get/register/leaderboard") {
csp::AppState::Instance().Init(":memory:");
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto user = auth.Register("contest_http_user", "password123");
csp::controllers::ContestController ctl;
auto list = CallList(ctl);
REQUIRE(list->statusCode() == drogon::k200OK);
auto list_json = list->jsonObject();
REQUIRE(list_json != nullptr);
REQUIRE((*list_json)["data"].isArray());
REQUIRE((*list_json)["data"].size() >= 1);
const int64_t contest_id = (*list_json)["data"][0]["id"].asInt64();
auto get = CallGet(ctl, contest_id, user.token);
REQUIRE(get->statusCode() == drogon::k200OK);
auto reg = CallRegister(ctl, contest_id, user.token);
REQUIRE(reg->statusCode() == drogon::k200OK);
auto board = CallBoard(ctl, contest_id);
REQUIRE(board->statusCode() == drogon::k200OK);
auto board_json = board->jsonObject();
REQUIRE(board_json != nullptr);
REQUIRE((*board_json)["data"].isArray());
}

查看文件

@@ -0,0 +1,75 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/auth_service.h"
#include "csp/services/contest_service.h"
#include "csp/services/submission_service.h"
namespace {
const char* kAcCode = R"CPP(#include <bits/stdc++.h>
using namespace std;
int main() {
long long a, b;
if (!(cin >> a >> b)) return 0;
cout << (a + b) << "\n";
return 0;
}
)CPP";
const char* kWaCode = R"CPP(#include <bits/stdc++.h>
using namespace std;
int main() {
cout << 0 << "\n";
return 0;
}
)CPP";
} // namespace
TEST_CASE("contest service leaderboard") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
csp::services::AuthService auth(db);
const auto u1 = auth.Register("contest_user_1", "password123");
const auto u2 = auth.Register("contest_user_2", "password123");
csp::services::ContestService contest(db);
const auto contests = contest.ListContests();
REQUIRE_FALSE(contests.empty());
const int64_t contest_id = contests.front().id;
contest.Register(contest_id, u1.user_id);
contest.Register(contest_id, u2.user_id);
const auto problems = contest.ListContestProblems(contest_id);
REQUIRE_FALSE(problems.empty());
const int64_t problem_id = problems.front().id;
csp::services::SubmissionService submissions(db);
csp::services::SubmissionCreateRequest r1;
r1.user_id = u1.user_id;
r1.problem_id = problem_id;
r1.contest_id = contest_id;
r1.language = "cpp";
r1.code = kAcCode;
const auto s1 = submissions.CreateAndJudge(r1);
REQUIRE(s1.status == csp::domain::SubmissionStatus::AC);
csp::services::SubmissionCreateRequest r2;
r2.user_id = u2.user_id;
r2.problem_id = problem_id;
r2.contest_id = contest_id;
r2.language = "cpp";
r2.code = kWaCode;
const auto s2 = submissions.CreateAndJudge(r2);
REQUIRE(s2.status == csp::domain::SubmissionStatus::WA);
const auto board = contest.Leaderboard(contest_id);
REQUIRE(board.size() >= 2);
REQUIRE(board.front().user_id == u1.user_id);
REQUIRE(board.front().solved >= 1);
}

查看文件

@@ -0,0 +1,60 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/import_controller.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallLatest(csp::controllers::ImportController& ctl) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.latestJob(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallItems(csp::controllers::ImportController& ctl, int64_t job_id) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->setParameter("page", "1");
req->setParameter("page_size", "20");
std::promise<drogon::HttpResponsePtr> p;
ctl.jobItems(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }, job_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("import controller latest and items") {
csp::AppState::Instance().Init(":memory:");
auto& db = csp::AppState::Instance().db();
db.Exec(
"INSERT INTO import_jobs(status,trigger,total_count,processed_count,success_count,"
"failed_count,options_json,last_error,started_at,finished_at,updated_at,created_at)"
"VALUES('success','auto',2,2,2,0,'{\"workers\":3}','',100,120,120,90);");
db.Exec(
"INSERT INTO import_job_items(job_id,source_path,status,title,difficulty,problem_id,"
"error_text,started_at,finished_at,updated_at,created_at)"
"VALUES(1,'CSP-J/2024/Round2/B.pdf','success','B',3,7,'',101,110,120,100);");
csp::controllers::ImportController ctl;
auto latest = CallLatest(ctl);
REQUIRE(latest->statusCode() == drogon::k200OK);
auto latest_json = latest->jsonObject();
REQUIRE(latest_json != nullptr);
REQUIRE((*latest_json)["ok"].asBool());
REQUIRE((*latest_json)["data"]["job"]["id"].asInt64() == 1);
auto items = CallItems(ctl, 1);
REQUIRE(items->statusCode() == drogon::k200OK);
auto items_json = items->jsonObject();
REQUIRE(items_json != nullptr);
REQUIRE((*items_json)["ok"].asBool());
REQUIRE((*items_json)["data"]["items"].isArray());
REQUIRE((*items_json)["data"]["items"].size() == 1);
}

查看文件

@@ -0,0 +1,34 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/import_service.h"
TEST_CASE("import service query latest job and items") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
db.Exec(
"INSERT INTO import_jobs(status,trigger,total_count,processed_count,success_count,"
"failed_count,options_json,last_error,started_at,finished_at,updated_at,created_at)"
"VALUES('running','manual',10,3,2,1,'{}','',100,null,120,90);");
db.Exec(
"INSERT INTO import_job_items(job_id,source_path,status,title,difficulty,problem_id,"
"error_text,started_at,finished_at,updated_at,created_at)"
"VALUES(1,'CSP-J/2024/Round1/A.pdf','success','A',2,11,'',101,102,120,100);");
csp::services::ImportService svc(db);
const auto latest = svc.GetLatestJob();
REQUIRE(latest.has_value());
REQUIRE(latest->id == 1);
REQUIRE(latest->status == "running");
REQUIRE(latest->processed_count == 3);
csp::services::ImportJobItemQuery q;
q.page = 1;
q.page_size = 20;
const auto items = svc.ListItems(1, q);
REQUIRE(items.size() == 1);
REQUIRE(items[0].source_path == "CSP-J/2024/Round1/A.pdf");
REQUIRE(items[0].status == "success");
REQUIRE(items[0].problem_id.has_value());
}

查看文件

@@ -0,0 +1,18 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/kb_service.h"
TEST_CASE("kb service list/detail") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
csp::services::KbService svc(db);
const auto rows = svc.ListArticles();
REQUIRE(rows.size() >= 2);
const auto detail = svc.GetBySlug(rows.front().slug);
REQUIRE(detail.has_value());
REQUIRE(detail->article.slug == rows.front().slug);
}

查看文件

@@ -0,0 +1,105 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/me_controller.h"
#include "csp/services/auth_service.h"
#include "csp/services/problem_service.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallProfile(csp::controllers::MeController& ctl,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.profile(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallListWrongBook(csp::controllers::MeController& ctl,
const std::string& token) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.listWrongBook(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallPatchWrongBook(csp::controllers::MeController& ctl,
const std::string& token,
int64_t problem_id,
const std::string& note) {
Json::Value body;
body["note"] = note;
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Patch);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.upsertWrongBookNote(req,
[&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
},
problem_id);
return p.get_future().get();
}
drogon::HttpResponsePtr CallDeleteWrongBook(csp::controllers::MeController& ctl,
const std::string& token,
int64_t problem_id) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Delete);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.deleteWrongBookItem(req,
[&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
},
problem_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("me controller profile and wrong-book") {
csp::AppState::Instance().Init(":memory:");
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto login = auth.Register("me_http_user", "password123");
csp::services::ProblemService problems(csp::AppState::Instance().db());
const auto list = problems.List(csp::services::ProblemQuery{});
REQUIRE_FALSE(list.items.empty());
const int64_t problem_id = list.items.front().id;
csp::controllers::MeController ctl;
auto profile = CallProfile(ctl, login.token);
REQUIRE(profile->statusCode() == drogon::k200OK);
auto profile_json = profile->jsonObject();
REQUIRE(profile_json != nullptr);
REQUIRE((*profile_json)["ok"].asBool());
auto patch = CallPatchWrongBook(ctl, login.token, problem_id, "复盘记录");
REQUIRE(patch->statusCode() == drogon::k200OK);
auto list_resp = CallListWrongBook(ctl, login.token);
REQUIRE(list_resp->statusCode() == drogon::k200OK);
auto list_json = list_resp->jsonObject();
REQUIRE(list_json != nullptr);
REQUIRE((*list_json)["data"].isArray());
REQUIRE((*list_json)["data"].size() >= 1);
auto del = CallDeleteWrongBook(ctl, login.token, problem_id);
REQUIRE(del->statusCode() == drogon::k200OK);
}

查看文件

@@ -0,0 +1,61 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/problem_controller.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallList(csp::controllers::ProblemController& ctl,
const std::string& page_size = "",
const std::string& tags = "") {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
if (!page_size.empty()) req->setParameter("page_size", page_size);
if (!tags.empty()) req->setParameter("tags", tags);
std::promise<drogon::HttpResponsePtr> p;
ctl.list(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallGet(csp::controllers::ProblemController& ctl,
int64_t problem_id) {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.getById(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
problem_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("problem controller list/get") {
csp::AppState::Instance().Init(":memory:");
csp::controllers::ProblemController ctl;
auto list_resp = CallList(ctl, "1");
REQUIRE(list_resp->statusCode() == drogon::k200OK);
auto list_json = list_resp->jsonObject();
REQUIRE(list_json != nullptr);
REQUIRE((*list_json)["ok"].asBool());
REQUIRE((*list_json)["data"]["items"].isArray());
REQUIRE((*list_json)["data"]["items"].size() == 1);
REQUIRE((*list_json)["data"]["total_count"].asInt() >= 1);
const int64_t problem_id = (*list_json)["data"]["items"][0]["id"].asInt64();
auto get_resp = CallGet(ctl, problem_id);
REQUIRE(get_resp->statusCode() == drogon::k200OK);
auto get_json = get_resp->jsonObject();
REQUIRE(get_json != nullptr);
REQUIRE((*get_json)["data"]["id"].asInt64() == problem_id);
}

查看文件

@@ -0,0 +1,50 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/problem_service.h"
TEST_CASE("problem service list/get") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
db.Exec(
"INSERT INTO problems(slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
"sample_input,sample_output,created_at)"
"VALUES('luogu-p1000','P1000','x',2,'luogu:P1000','https://www.luogu.com.cn/problem/P1000','{}','','',100);");
db.Exec(
"INSERT INTO problems(slug,title,statement_md,difficulty,source,statement_url,llm_profile_json,"
"sample_input,sample_output,created_at)"
"VALUES('luogu-p1001','P1001','x',3,'luogu:P1001','https://www.luogu.com.cn/problem/P1001','{}','','',100);");
db.Exec("INSERT INTO problem_tags(problem_id,tag) VALUES((SELECT id FROM problems WHERE slug='luogu-p1000'),'csp-j');");
db.Exec("INSERT INTO problem_tags(problem_id,tag) VALUES((SELECT id FROM problems WHERE slug='luogu-p1001'),'csp-s');");
csp::services::ProblemService svc(db);
csp::services::ProblemQuery q_all;
const auto all = svc.List(q_all);
REQUIRE(all.total_count >= 5);
REQUIRE(all.items.size() >= 5);
csp::services::ProblemQuery q_dp;
q_dp.tag = "dp";
const auto dp = svc.List(q_dp);
REQUIRE(dp.total_count >= 1);
REQUIRE(dp.items.size() >= 1);
csp::services::ProblemQuery q_source;
q_source.source_prefix = "luogu:";
const auto luogu = svc.List(q_source);
REQUIRE(luogu.total_count == 2);
REQUIRE(luogu.items.size() == 2);
csp::services::ProblemQuery q_tags;
q_tags.tags = {"csp-j", "csp-s"};
const auto csp = svc.List(q_tags);
REQUIRE(csp.total_count == 2);
REQUIRE(csp.items.size() == 2);
const auto one = svc.GetById(all.items.front().id);
REQUIRE(one.has_value());
REQUIRE(one->id == all.items.front().id);
}

查看文件

@@ -0,0 +1,89 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/problem_controller.h"
#include "csp/services/auth_service.h"
#include <drogon/HttpRequest.h>
#include <sqlite3.h>
#include <future>
namespace {
int64_t FirstProblemId() {
sqlite3_stmt* stmt = nullptr;
auto* db = csp::AppState::Instance().db().raw();
REQUIRE(sqlite3_prepare_v2(db, "SELECT id FROM problems ORDER BY id LIMIT 1", -1,
&stmt, nullptr) == SQLITE_OK);
REQUIRE(sqlite3_step(stmt) == SQLITE_ROW);
const auto id = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return id;
}
std::string NewToken() {
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto r = auth.Register("workspace_tester", "password123");
return r.token;
}
} // namespace
TEST_CASE("problem controller draft and solution list") {
csp::AppState::Instance().Init(":memory:");
csp::controllers::ProblemController ctl;
const auto problem_id = FirstProblemId();
const auto token = NewToken();
{
Json::Value body;
body["language"] = "cpp";
body["code"] = "int main(){return 0;}";
body["stdin"] = "1 2\n";
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Put);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.saveDraft(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
problem_id);
auto resp = p.get_future().get();
REQUIRE(resp->statusCode() == drogon::k200OK);
}
{
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.getDraft(req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
problem_id);
auto resp = p.get_future().get();
REQUIRE(resp->statusCode() == drogon::k200OK);
auto json = resp->jsonObject();
REQUIRE(json != nullptr);
REQUIRE((*json)["data"]["code"].asString().find("main") != std::string::npos);
}
{
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
std::promise<drogon::HttpResponsePtr> p;
ctl.listSolutions(
req,
[&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); },
problem_id);
auto resp = p.get_future().get();
REQUIRE(resp->statusCode() == drogon::k200OK);
auto json = resp->jsonObject();
REQUIRE(json != nullptr);
REQUIRE((*json)["data"]["items"].isArray());
}
}

查看文件

@@ -0,0 +1,48 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/problem_workspace_service.h"
#include <sqlite3.h>
TEST_CASE("problem workspace service drafts and solution jobs") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
REQUIRE(sqlite3_exec(
db.raw(),
"INSERT INTO users(username,password_salt,password_hash,created_at)"
" VALUES('tester','salt','hash',0)",
nullptr,
nullptr,
nullptr) == SQLITE_OK);
sqlite3_stmt* stmt = nullptr;
REQUIRE(sqlite3_prepare_v2(db.raw(), "SELECT id FROM problems ORDER BY id LIMIT 1",
-1, &stmt, nullptr) == SQLITE_OK);
REQUIRE(sqlite3_step(stmt) == SQLITE_ROW);
const int64_t pid = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
REQUIRE(pid > 0);
csp::services::ProblemWorkspaceService svc(db);
REQUIRE(svc.ProblemExists(pid));
svc.SaveDraft(1, pid, "cpp", "int main(){return 0;}", "1 2\n");
const auto draft = svc.GetDraft(1, pid);
REQUIRE(draft.has_value());
REQUIRE(draft->language == "cpp");
REQUIRE(draft->code.find("main") != std::string::npos);
const auto job_id = svc.CreateSolutionJob(pid, 1, 3);
REQUIRE(job_id > 0);
const auto latest = svc.GetLatestSolutionJob(pid);
REQUIRE(latest.has_value());
REQUIRE(latest->id == job_id);
REQUIRE(latest->status == "queued");
REQUIRE(latest->max_solutions == 3);
const auto solutions = svc.ListSolutions(pid);
REQUIRE(solutions.empty());
}

查看文件

@@ -28,6 +28,17 @@ TEST_CASE("migrations create core tables") {
REQUIRE(CountTable(db.raw(), "users") == 1);
REQUIRE(CountTable(db.raw(), "sessions") == 1);
REQUIRE(CountTable(db.raw(), "problems") == 1);
REQUIRE(CountTable(db.raw(), "problem_tags") == 1);
REQUIRE(CountTable(db.raw(), "submissions") == 1);
REQUIRE(CountTable(db.raw(), "wrong_book") == 1);
REQUIRE(CountTable(db.raw(), "contests") == 1);
REQUIRE(CountTable(db.raw(), "contest_problems") == 1);
REQUIRE(CountTable(db.raw(), "contest_registrations") == 1);
REQUIRE(CountTable(db.raw(), "kb_articles") == 1);
REQUIRE(CountTable(db.raw(), "kb_article_links") == 1);
REQUIRE(CountTable(db.raw(), "import_jobs") == 1);
REQUIRE(CountTable(db.raw(), "import_job_items") == 1);
REQUIRE(CountTable(db.raw(), "problem_drafts") == 1);
REQUIRE(CountTable(db.raw(), "problem_solution_jobs") == 1);
REQUIRE(CountTable(db.raw(), "problem_solutions") == 1);
}

查看文件

@@ -0,0 +1,80 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/app_state.h"
#include "csp/controllers/submission_controller.h"
#include "csp/services/auth_service.h"
#include "csp/services/problem_service.h"
#include <drogon/HttpRequest.h>
#include <future>
namespace {
drogon::HttpResponsePtr CallRun(csp::controllers::SubmissionController& ctl,
const std::string& code,
const std::string& input) {
Json::Value body;
body["code"] = code;
body["input"] = input;
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Post);
std::promise<drogon::HttpResponsePtr> p;
ctl.runCpp(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
return p.get_future().get();
}
drogon::HttpResponsePtr CallSubmit(csp::controllers::SubmissionController& ctl,
int64_t problem_id,
const std::string& token,
const std::string& code) {
Json::Value body;
body["language"] = "cpp";
body["code"] = code;
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
req->setMethod(drogon::Post);
req->addHeader("Authorization", "Bearer " + token);
std::promise<drogon::HttpResponsePtr> p;
ctl.submitProblem(req,
[&p](const drogon::HttpResponsePtr& resp) {
p.set_value(resp);
},
problem_id);
return p.get_future().get();
}
} // namespace
TEST_CASE("submission controller run and submit") {
csp::AppState::Instance().Init(":memory:");
csp::services::AuthService auth(csp::AppState::Instance().db());
const auto user = auth.Register("submission_http_user", "password123");
csp::services::ProblemService problems(csp::AppState::Instance().db());
const auto list = problems.List(csp::services::ProblemQuery{});
REQUIRE_FALSE(list.items.empty());
csp::controllers::SubmissionController ctl;
auto run = CallRun(ctl,
"#include <iostream>\nint main(){std::cout<<\"ok\\n\";}",
"");
REQUIRE(run->statusCode() == drogon::k200OK);
auto submit = CallSubmit(
ctl,
list.items.front().id,
user.token,
R"CPP(#include <bits/stdc++.h>
using namespace std;
int main(){ long long a,b; if(!(cin>>a>>b)) return 0; cout << (a+b) << "\n"; }
)CPP");
REQUIRE(submit->statusCode() == drogon::k200OK);
auto submit_json = submit->jsonObject();
REQUIRE(submit_json != nullptr);
REQUIRE((*submit_json)["data"]["id"].asInt64() > 0);
}

查看文件

@@ -0,0 +1,86 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/domain/enum_strings.h"
#include "csp/services/auth_service.h"
#include "csp/services/problem_service.h"
#include "csp/services/submission_service.h"
#include "csp/services/wrong_book_service.h"
namespace {
const char* kAcCode = R"CPP(#include <bits/stdc++.h>
using namespace std;
int main() {
long long a, b;
if (!(cin >> a >> b)) return 0;
cout << (a + b) << "\n";
return 0;
}
)CPP";
const char* kWaCode = R"CPP(#include <bits/stdc++.h>
using namespace std;
int main() {
cout << 0 << "\n";
return 0;
}
)CPP";
} // namespace
TEST_CASE("submission service judge and wrong-book flow") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::db::SeedDemoData(db);
csp::services::AuthService auth(db);
const auto user = auth.Register("submit_user", "password123");
csp::services::ProblemService problems(db);
csp::services::ProblemQuery q;
const auto list = problems.List(q);
REQUIRE_FALSE(list.items.empty());
const int64_t problem_id = list.items.front().id;
csp::services::SubmissionService submissions(db);
csp::services::WrongBookService wrong_book(db);
csp::services::SubmissionCreateRequest bad;
bad.user_id = user.user_id;
bad.problem_id = problem_id;
bad.language = "cpp";
bad.code = kWaCode;
const auto bad_result = submissions.CreateAndJudge(bad);
REQUIRE(bad_result.status == csp::domain::SubmissionStatus::WA);
const auto wb_after_bad = wrong_book.ListByUser(user.user_id);
REQUIRE_FALSE(wb_after_bad.empty());
csp::services::SubmissionCreateRequest good;
good.user_id = user.user_id;
good.problem_id = problem_id;
good.language = "cpp";
good.code = kAcCode;
const auto good_result = submissions.CreateAndJudge(good);
REQUIRE(good_result.status == csp::domain::SubmissionStatus::AC);
bool still_in_wrong_book = false;
for (const auto& row : wrong_book.ListByUser(user.user_id)) {
if (row.item.problem_id == problem_id) {
still_in_wrong_book = true;
break;
}
}
REQUIRE_FALSE(still_in_wrong_book);
const auto run_only =
submissions.RunOnlyCpp(R"CPP(#include <iostream>
int main(){std::cout<<42<<"\n";}
)CPP",
"");
REQUIRE(run_only.status == csp::domain::SubmissionStatus::Running);
REQUIRE(run_only.stdout_text == "42\n");
}