Compare commits
1 commit
develop
...
feature/GA
Author | SHA1 | Date | |
---|---|---|---|
|
4fb0b80f24 |
17 changed files with 1514 additions and 410 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
|||
[submodule "proto"]
|
||||
path = contracts
|
||||
path = proto
|
||||
url = https://git.sch9.ru/new_gate/contracts
|
||||
|
|
2
Makefile
2
Makefile
|
@ -1,7 +1,7 @@
|
|||
tag = latest
|
||||
|
||||
gen:
|
||||
@oapi-codegen --config=config.yaml ./contracts/tester/v1/openapi.yaml
|
||||
@oapi-codegen --config=config.yaml ./proto/tester/v1/openapi.yaml
|
||||
dev: gen
|
||||
@go run main.go
|
||||
build: gen
|
||||
|
|
108
README.md
108
README.md
|
@ -1,108 +0,0 @@
|
|||
# ms-tester
|
||||
|
||||
`ms-tester` is a microservice designed for managing programming competitions. It provides backend functionality for handling problems, contests, participants, and their submissions. The service is developed in Go. PostgreSQL serves as the relational database. Pandoc is used to convert problem statements from LaTeX to HTML.
|
||||
|
||||
For understanding the architecture, see the [documentation](https://git.sch9.ru/new_gate/docs).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following dependencies installed:
|
||||
|
||||
* **Docker** and **Docker Compose**: To run PostgreSQL, Pandoc.
|
||||
* **Goose**: For applying database migrations (`go install github.com/pressly/goose/v3/cmd/goose@latest`).
|
||||
* **oapi-codegen**: For generating OpenAPI code (`go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest`).
|
||||
|
||||
### 1. Running Dependencies
|
||||
|
||||
You can run PostgreSQL and Pandoc using Docker Compose, for example:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
pandoc:
|
||||
image: pandoc/latex
|
||||
ports:
|
||||
- "4000:3030" # Exposes Pandoc server on port 4000 locally
|
||||
command: "server" # Runs Pandoc in server mode
|
||||
postgres:
|
||||
image: postgres:14.1-alpine # Uses PostgreSQL 14.1 Alpine image
|
||||
restart: always # Ensures the container restarts if it stops
|
||||
environment:
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: supersecretpassword # Default password (change for production!)
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
ports:
|
||||
- '5432:5432' # Exposes PostgreSQL on the standard port 5432
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data # Persists database data locally
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -d postgres # Command to check if PostgreSQL is ready
|
||||
interval: 10s # Check every 10 seconds
|
||||
timeout: 3s # Wait 3 seconds for the check to respond
|
||||
retries: 5 # Try 5 times before marking as unhealthy
|
||||
volumes:
|
||||
postgres-data: # Defines the named volume for data persistence
|
||||
```
|
||||
|
||||
Start the services in detached mode:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
The application uses environment variables for configuration. Create a `.env` file in the project root. The minimum required variables are:
|
||||
|
||||
```dotenv
|
||||
# Environment type (development or production)
|
||||
ENV=dev # or prod
|
||||
|
||||
# Address of the running Pandoc service
|
||||
PANDOC=http://localhost:4000
|
||||
|
||||
# Address and port where the ms-tester service will listen
|
||||
ADDRESS=localhost:8080
|
||||
|
||||
# PostgreSQL connection string (Data Source Name)
|
||||
# Format: postgres://user:password@host:port/database?sslmode=disable
|
||||
POSTGRES_DSN=postgres://username:supersecretpassword@localhost:5432/db_name?sslmode=disable
|
||||
|
||||
# Secret key for signing and verifying JWT tokens
|
||||
JWT_SECRET=your_super_secret_jwt_key
|
||||
```
|
||||
|
||||
**Important:** Replace `supersecretpassword` and `your_super_secret_jwt_key` with secure, unique values, especially for a production environment.
|
||||
|
||||
### 3. Database Migrations
|
||||
|
||||
The project uses `goose` to manage the database schema.
|
||||
|
||||
1. Ensure `goose` is installed:
|
||||
```bash
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
```
|
||||
2. Apply the migrations to the running PostgreSQL database. Make sure the connection string in the command matches the `POSTGRES_DSN` from your `.env` file:
|
||||
```bash
|
||||
goose -dir ./migrations postgres "postgres://postgres:supersecretpassword@localhost:5432/postgres?sslmode=disable" up
|
||||
```
|
||||
|
||||
### 4. OpenAPI Code Generation
|
||||
|
||||
The project uses OpenAPI to define its API. Go code for handlers and models is generated based on this specification using `oapi-codegen`.
|
||||
|
||||
Run the generation command:
|
||||
|
||||
```bash
|
||||
make gen
|
||||
```
|
||||
|
||||
### 5. Running the Application
|
||||
|
||||
Start the `ms-tester` service:
|
||||
|
||||
```bash
|
||||
go run ./main.go
|
||||
```
|
||||
|
||||
After starting, the service will be available at the address specified in the `ADDRESS` variable in your `.env` file (e.g., `http://localhost:8080`).
|
|
@ -2,4 +2,4 @@ package: testerv1
|
|||
generate:
|
||||
fiber-server: true
|
||||
models: true
|
||||
output: ./contracts/tester/v1/tester.go
|
||||
output: ./proto/tester/v1/tester.go
|
5
go.mod
5
go.mod
|
@ -4,11 +4,13 @@ go 1.23.6
|
|||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/open-policy-agent/opa v1.2.0
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
|
@ -35,10 +37,11 @@ require (
|
|||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.21.0 // indirect
|
||||
|
|
7
go.sum
7
go.sum
|
@ -5,6 +5,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
|
|||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
|
@ -97,6 +99,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
@ -146,6 +152,7 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
|||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package tester
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"io"
|
||||
)
|
||||
|
@ -99,15 +99,6 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error {
|
|||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
var participantId int32 = 2
|
||||
|
||||
solutions, err := h.contestsUC.ReadBestSolutions(c.Context(), id, participantId)
|
||||
|
||||
m := make(map[int32]*models.Solution)
|
||||
|
||||
for i := 0; i < len(solutions); i++ {
|
||||
m[solutions[i].TaskPosition] = solutions[i]
|
||||
}
|
||||
resp := testerv1.GetContestResponse{
|
||||
Contest: C2C(*contest),
|
||||
Tasks: make([]struct {
|
||||
|
@ -117,15 +108,11 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error {
|
|||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
solution := testerv1.Solution{}
|
||||
if sol, ok := m[task.Position]; ok {
|
||||
solution = S2S(*sol)
|
||||
}
|
||||
resp.Tasks[i] = struct {
|
||||
Solution testerv1.Solution `json:"solution"`
|
||||
Task testerv1.TasksListItem `json:"task"`
|
||||
}{
|
||||
Solution: solution,
|
||||
Solution: testerv1.Solution{},
|
||||
Task: TLI2TLI(*task),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,5 +49,4 @@ type ContestRepository interface {
|
|||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
||||
|
|
|
@ -2,10 +2,9 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ContestRepository struct {
|
||||
|
@ -172,7 +171,7 @@ const (
|
|||
func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
|
||||
const op = "ContestRepository.ReadTasks"
|
||||
|
||||
var contests []*models.ContestsListItem
|
||||
contests := make([]*models.ContestsListItem, 0)
|
||||
query := r.db.Rebind(readContestsListQuery)
|
||||
err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
|
@ -207,7 +206,7 @@ func (r *ContestRepository) ListParticipants(ctx context.Context, filter models.
|
|||
filter.PageSize = 1
|
||||
}
|
||||
|
||||
var participants []*models.ParticipantsListItem
|
||||
participants := make([]*models.ParticipantsListItem, 0)
|
||||
query := r.db.Rebind(readParticipantsListQuery)
|
||||
err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset())
|
||||
if err != nil {
|
||||
|
@ -313,93 +312,94 @@ func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models
|
|||
return id, nil
|
||||
}
|
||||
|
||||
// buildListSolutionsQueries builds two SQL queries: one for selecting solutions
|
||||
// and another for counting them. The first query selects all columns that are
|
||||
// needed for the solutions list, including the task and contest titles, and
|
||||
// the participant name. The second query counts the number of solutions that
|
||||
// match the filter.
|
||||
//
|
||||
// The caller is responsible for executing the queries and processing the
|
||||
// results.
|
||||
func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
|
||||
columns := []string{
|
||||
"s.id",
|
||||
"s.participant_id",
|
||||
"p2.name AS participant_name",
|
||||
"s.state",
|
||||
"s.score",
|
||||
"s.penalty",
|
||||
"s.time_stat",
|
||||
"s.memory_stat",
|
||||
"s.language",
|
||||
"s.task_id",
|
||||
"t.position AS task_position",
|
||||
"p.title AS task_title",
|
||||
"t.contest_id",
|
||||
"c.title",
|
||||
"s.updated_at",
|
||||
"s.created_at",
|
||||
}
|
||||
|
||||
qb := sq.Select(columns...).
|
||||
From("solutions s").
|
||||
LeftJoin("tasks t ON s.task_id = t.id").
|
||||
LeftJoin("problems p ON t.problem_id = p.id").
|
||||
LeftJoin("contests c ON t.contest_id = c.id").
|
||||
LeftJoin("participants p2 ON s.participant_id = p2.id")
|
||||
|
||||
if filter.ContestId != nil {
|
||||
qb = qb.Where("s.contest_id = ?", *filter.ContestId)
|
||||
}
|
||||
if filter.ParticipantId != nil {
|
||||
qb = qb.Where("s.participant_id = ?", *filter.ParticipantId)
|
||||
}
|
||||
if filter.TaskId != nil {
|
||||
qb = qb.Where("s.task_id = ?", *filter.TaskId)
|
||||
}
|
||||
if filter.Language != nil {
|
||||
qb = qb.Where("s.language = ?", *filter.Language)
|
||||
}
|
||||
if filter.State != nil {
|
||||
qb = qb.Where("s.state = ?", *filter.State)
|
||||
}
|
||||
|
||||
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
|
||||
|
||||
if filter.Order != nil && *filter.Order < 0 {
|
||||
qb = qb.OrderBy("s.id DESC")
|
||||
}
|
||||
|
||||
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
|
||||
|
||||
return qb, countQb
|
||||
}
|
||||
|
||||
func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
|
||||
const op = "ContestRepository.ListSolutions"
|
||||
|
||||
baseQuery := `
|
||||
SELECT s.id,
|
||||
baseQb, countQb := buildListSolutionsQueries(filter)
|
||||
|
||||
s.participant_id,
|
||||
p2.name as participant_name,
|
||||
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
|
||||
s.task_id,
|
||||
t.position as task_position,
|
||||
p.title as task_title,
|
||||
|
||||
t.contest_id,
|
||||
c.title,
|
||||
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM solutions s
|
||||
LEFT JOIN tasks t ON s.task_id = t.id
|
||||
LEFT JOIN problems p ON t.problem_id = p.id
|
||||
LEFT JOIN contests c ON t.contest_id = c.id
|
||||
LEFT JOIN participants p2 on s.participant_id = p2.id
|
||||
WHERE 1=1
|
||||
`
|
||||
|
||||
var conditions []string
|
||||
var args []interface{}
|
||||
|
||||
if filter.ContestId != nil {
|
||||
conditions = append(conditions, "s.contest_id = ?")
|
||||
args = append(args, *filter.ContestId)
|
||||
query, args, err := countQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
if filter.ParticipantId != nil {
|
||||
conditions = append(conditions, "s.participant_id = ?")
|
||||
args = append(args, *filter.ParticipantId)
|
||||
}
|
||||
if filter.TaskId != nil {
|
||||
conditions = append(conditions, "s.task_id = ?")
|
||||
args = append(args, *filter.TaskId)
|
||||
}
|
||||
if filter.Language != nil {
|
||||
conditions = append(conditions, "s.language = ?")
|
||||
args = append(args, *filter.Language)
|
||||
}
|
||||
if filter.State != nil {
|
||||
conditions = append(conditions, "s.state = ?")
|
||||
args = append(args, *filter.State)
|
||||
}
|
||||
|
||||
if len(conditions) > 0 {
|
||||
baseQuery += " AND " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
if filter.Order != nil {
|
||||
orderDirection := "ASC"
|
||||
if *filter.Order < 0 {
|
||||
orderDirection = "DESC"
|
||||
}
|
||||
baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection)
|
||||
}
|
||||
|
||||
countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table"
|
||||
var totalCount int32
|
||||
err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount)
|
||||
err = r.db.QueryRowxContext(ctx, r.db.Rebind(query), args...).Scan(&totalCount)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
baseQuery += " LIMIT ? OFFSET ?"
|
||||
args = append(args, filter.PageSize, offset)
|
||||
|
||||
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...)
|
||||
query, args, err = baseQb.ToSql()
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(query), args...)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var solutions []*models.SolutionsListItem
|
||||
solutions := make([]*models.SolutionsListItem, 0)
|
||||
for rows.Next() {
|
||||
var solution models.SolutionsListItem
|
||||
err = rows.StructScan(&solution)
|
||||
|
@ -551,7 +551,7 @@ WITH Attempts AS (
|
|||
MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty
|
||||
FROM solutions s
|
||||
JOIN tasks t ON t.id = s.task_id
|
||||
WHERE t.contest_id = :contest_id
|
||||
WHERE t.contest_id = ?
|
||||
GROUP BY s.participant_id, s.task_id
|
||||
)
|
||||
SELECT
|
||||
|
@ -559,11 +559,11 @@ SELECT
|
|||
p.name,
|
||||
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
|
||||
COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL
|
||||
THEN a.failed_attempts * :penalty + a.success_penalty
|
||||
THEN a.failed_attempts * ? + a.success_penalty
|
||||
ELSE 0 END), 0) as penalty_in_total
|
||||
FROM participants p
|
||||
LEFT JOIN Attempts a ON a.participant_id = p.id
|
||||
WHERE p.contest_id = :contest_id
|
||||
WHERE p.contest_id = ?
|
||||
GROUP BY p.id, p.name
|
||||
`
|
||||
)
|
||||
|
@ -596,10 +596,7 @@ func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*
|
|||
|
||||
penalty := int32(20) // FIXME
|
||||
namedQuery := r.db.Rebind(participantsQuery)
|
||||
rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{
|
||||
"contest_id": contestId,
|
||||
"penalty": penalty,
|
||||
})
|
||||
rows3, err := r.db.QueryxContext(ctx, namedQuery, contestId, penalty, contestId)
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
@ -626,65 +623,3 @@ func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*
|
|||
|
||||
return &monitor, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// state=5 - AC
|
||||
readBestSolutions = `
|
||||
WITH contest_tasks AS (
|
||||
SELECT t.id AS task_id,
|
||||
t.position AS task_position,
|
||||
t.contest_id,
|
||||
t.problem_id,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
p.title AS task_title,
|
||||
c.title AS contest_title
|
||||
FROM tasks t
|
||||
LEFT JOIN problems p ON p.id = t.problem_id
|
||||
LEFT JOIN contests c ON c.id = t.contest_id
|
||||
WHERE t.contest_id = ?
|
||||
),
|
||||
best_solutions AS (
|
||||
SELECT DISTINCT ON (s.task_id)
|
||||
*
|
||||
FROM solutions s
|
||||
WHERE s.participant_id = ?
|
||||
ORDER BY s.task_id, s.score DESC, s.created_at DESC
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.participant_id,
|
||||
p.name AS participant_name,
|
||||
s.solution,
|
||||
s.state,
|
||||
s.score,
|
||||
s.penalty,
|
||||
s.time_stat,
|
||||
s.memory_stat,
|
||||
s.language,
|
||||
ct.task_id,
|
||||
ct.task_position,
|
||||
ct.task_title,
|
||||
ct.contest_id,
|
||||
ct.contest_title,
|
||||
s.updated_at,
|
||||
s.created_at
|
||||
FROM contest_tasks ct
|
||||
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
|
||||
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
|
||||
ORDER BY ct.task_position
|
||||
`
|
||||
)
|
||||
|
||||
func (r *ContestRepository) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
const op = "ContestRepository.ReadBestSolutions"
|
||||
var solutions []*models.Solution
|
||||
query := r.db.Rebind(readBestSolutions)
|
||||
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
|
||||
|
||||
if err != nil {
|
||||
return nil, handlePgErr(err, op)
|
||||
}
|
||||
|
||||
return solutions, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,57 +2,153 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProblemRepository_CreateProblem(t *testing.T) {
|
||||
t.Parallel()
|
||||
type problemTestFixture struct {
|
||||
db *sql.DB
|
||||
sqlxDB *sqlx.DB
|
||||
mock sqlmock.Sqlmock
|
||||
problemRepo *ProblemRepository
|
||||
}
|
||||
|
||||
func newProblemTestFixture(t *testing.T) *problemTestFixture {
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
repo := NewProblemRepository(sqlxDB)
|
||||
|
||||
problemRepo := NewProblemRepository(sqlxDB, zap.NewNop())
|
||||
return &problemTestFixture{
|
||||
db: db,
|
||||
sqlxDB: sqlxDB,
|
||||
mock: mock,
|
||||
problemRepo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup closes database connections
|
||||
func (tf *problemTestFixture) cleanup() {
|
||||
tf.db.Close()
|
||||
tf.sqlxDB.Close()
|
||||
}
|
||||
|
||||
func TestProblemRepository_CreateProblem(t *testing.T) {
|
||||
tf := newProblemTestFixture(t)
|
||||
defer tf.cleanup()
|
||||
|
||||
t.Run("valid problem creation", func(t *testing.T) {
|
||||
title := "Problem title"
|
||||
|
||||
title := "Test Problem"
|
||||
rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
|
||||
|
||||
mock.ExpectQuery(sqlxDB.Rebind(createProblemQuery)).WithArgs(title).WillReturnRows(rows)
|
||||
tf.mock.ExpectQuery(tf.sqlxDB.Rebind(createProblemQuery)).
|
||||
WithArgs(title).
|
||||
WillReturnRows(rows)
|
||||
|
||||
id, err := problemRepo.CreateProblem(context.Background(), title)
|
||||
id, err := tf.problemRepo.CreateProblem(context.Background(), tf.problemRepo.DB(), title)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProblemRepository_DeleteProblem(t *testing.T) {
|
||||
t.Parallel()
|
||||
func TestProblemRepository_ReadProblemById(t *testing.T) {
|
||||
tf := newProblemTestFixture(t)
|
||||
defer tf.cleanup()
|
||||
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
t.Run("valid problem read", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewRows([]string{"id", "title"}).
|
||||
AddRow(1, "Test Problem")
|
||||
|
||||
tf.mock.ExpectQuery(tf.sqlxDB.Rebind(readProblemQuery)).
|
||||
WithArgs(id).
|
||||
WillReturnRows(rows)
|
||||
|
||||
problem, err := tf.problemRepo.ReadProblemById(context.Background(), tf.problemRepo.DB(), id)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
require.NotNil(t, problem)
|
||||
require.Equal(t, int32(1), problem.Id)
|
||||
require.Equal(t, "Test Problem", problem.Title)
|
||||
})
|
||||
}
|
||||
|
||||
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
||||
defer sqlxDB.Close()
|
||||
|
||||
problemRepo := NewProblemRepository(sqlxDB, zap.NewNop())
|
||||
func TestProblemRepository_DeleteProblem(t *testing.T) {
|
||||
tf := newProblemTestFixture(t)
|
||||
defer tf.cleanup()
|
||||
|
||||
t.Run("valid problem deletion", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
mock.ExpectExec(sqlxDB.Rebind(deleteProblemQuery)).WithArgs(id).WillReturnResult(rows)
|
||||
tf.mock.ExpectExec(tf.sqlxDB.Rebind(deleteProblemQuery)).
|
||||
WithArgs(id).
|
||||
WillReturnResult(rows)
|
||||
|
||||
err = problemRepo.DeleteProblem(context.Background(), id)
|
||||
err := tf.problemRepo.DeleteProblem(context.Background(), tf.problemRepo.DB(), id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProblemRepository_ListProblems(t *testing.T) {
|
||||
tf := newProblemTestFixture(t)
|
||||
defer tf.cleanup()
|
||||
|
||||
t.Run("valid problems list", func(t *testing.T) {
|
||||
filter := models.ProblemsFilter{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
listRows := sqlmock.NewRows([]string{"id", "title", "solved_count"}).
|
||||
AddRow(1, "Problem 1", 5).
|
||||
AddRow(2, "Problem 2", 3)
|
||||
countRows := sqlmock.NewRows([]string{"count"}).AddRow(2)
|
||||
|
||||
tf.mock.ExpectQuery(tf.sqlxDB.Rebind(ListProblemsQuery)).
|
||||
WithArgs(filter.PageSize, filter.Offset()).
|
||||
WillReturnRows(listRows)
|
||||
|
||||
tf.mock.ExpectQuery(tf.sqlxDB.Rebind(CountProblemsQuery)).
|
||||
WillReturnRows(countRows)
|
||||
|
||||
result, err := tf.problemRepo.ListProblems(context.Background(), tf.problemRepo.DB(), filter)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Problems, 2)
|
||||
require.Equal(t, int32(1), result.Pagination.Page)
|
||||
require.Equal(t, int32(1), result.Pagination.Total)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProblemRepository_UpdateProblem(t *testing.T) {
|
||||
tf := newProblemTestFixture(t)
|
||||
defer tf.cleanup()
|
||||
|
||||
t.Run("valid problem update", func(t *testing.T) {
|
||||
id := int32(1)
|
||||
problemUpdate := models.ProblemUpdate{
|
||||
Title: sp("Updated Title"),
|
||||
TimeLimit: i32p(1000),
|
||||
MemoryLimit: i32p(256),
|
||||
}
|
||||
rows := sqlmock.NewResult(1, 1)
|
||||
|
||||
tf.mock.ExpectExec(tf.sqlxDB.Rebind(UpdateProblemQuery)).
|
||||
WithArgs(
|
||||
problemUpdate.Title,
|
||||
problemUpdate.TimeLimit,
|
||||
problemUpdate.MemoryLimit,
|
||||
nil, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, nil,
|
||||
id,
|
||||
).
|
||||
WillReturnResult(rows)
|
||||
|
||||
err := tf.problemRepo.UpdateProblem(context.Background(), tf.problemRepo.DB(), id, problemUpdate)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -31,5 +31,4 @@ type ContestUseCase interface {
|
|||
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
|
||||
ReadTask(ctx context.Context, id int32) (*models.Task, error)
|
||||
ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error)
|
||||
ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error)
|
||||
}
|
||||
|
|
|
@ -85,7 +85,3 @@ func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task,
|
|||
func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
|
||||
return uc.contestRepo.ReadMonitor(ctx, contestId)
|
||||
}
|
||||
|
||||
func (uc *ContestUseCase) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) {
|
||||
return uc.contestRepo.ReadBestSolutions(ctx, contestId, participantId)
|
||||
}
|
||||
|
|
2
main.go
2
main.go
|
@ -3,11 +3,11 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/tester/delivery/rest"
|
||||
problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/tester/repository"
|
||||
testerUseCase "git.sch9.ru/new_gate/ms-tester/internal/tester/usecase"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
|
|
|
@ -9,24 +9,6 @@ BEGIN
|
|||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION check_max_tasks() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS
|
||||
$$
|
||||
DECLARE
|
||||
max_on_contest_tasks_amount integer := 50;
|
||||
BEGIN
|
||||
IF (
|
||||
SELECT count(*) FROM tasks
|
||||
WHERE contest_id = NEW.contest_id
|
||||
) >= (
|
||||
max_on_contest_tasks_amount
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Exceeded max tasks for this contest';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS problems
|
||||
(
|
||||
id serial NOT NULL,
|
||||
|
@ -91,11 +73,6 @@ CREATE TABLE IF NOT EXISTS tasks
|
|||
CHECK (position >= 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER max_tasks_on_contest_check
|
||||
BEFORE INSERT ON tasks
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION check_max_tasks();
|
||||
|
||||
CREATE TRIGGER on_tasks_update
|
||||
BEFORE UPDATE
|
||||
ON tasks
|
||||
|
@ -121,7 +98,6 @@ CREATE TRIGGER on_participants_update
|
|||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
|
@ -149,17 +125,15 @@ EXECUTE PROCEDURE updated_at_update();
|
|||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS on_solutions_update ON solutions;
|
||||
DROP TABLE IF EXISTS solutions;
|
||||
DROP TRIGGER IF EXISTS on_participants_update ON participants;
|
||||
DROP TABLE IF EXISTS participants;
|
||||
DROP TRIGGER IF EXISTS on_tasks_update ON tasks;
|
||||
DROP TRIGGER IF EXISTS max_tasks_on_contest_check ON tasks;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
DROP TRIGGER IF EXISTS on_problems_update ON problems;
|
||||
DROP TABLE IF EXISTS problems;
|
||||
DROP TRIGGER IF EXISTS on_contests_update ON contests;
|
||||
DROP TABLE IF EXISTS contests;
|
||||
DROP FUNCTION IF EXISTS updated_at_update();
|
||||
DROP FUNCTION IF EXISTS check_max_tasks();
|
||||
DROP TRIGGER IF EXISTS on_tasks_update ON tasks;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
DROP TRIGGER IF EXISTS on_participants_update ON participants;
|
||||
DROP TABLE IF EXISTS participants;
|
||||
DROP TRIGGER IF EXISTS on_solutions_update ON solutions;
|
||||
DROP TABLE IF EXISTS solutions;
|
||||
DROP FUNCTION updated_at_update();
|
||||
-- +goose StatementEnd
|
Loading…
Add table
Reference in a new issue