feat: rebuild CSP practice workflow, UX and automation
这个提交包含在:
@@ -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());
|
||||
}
|
||||
18
backend/tests/kb_service_test.cc
普通文件
18
backend/tests/kb_service_test.cc
普通文件
@@ -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);
|
||||
}
|
||||
105
backend/tests/me_http_test.cc
普通文件
105
backend/tests/me_http_test.cc
普通文件
@@ -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");
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户