├── lib ├── config │ ├── config.go │ ├── elastic.go │ ├── redis.go │ ├── email.go │ ├── http.go │ ├── s3.go │ └── mysql.go ├── life │ └── life.go ├── util │ └── util.go ├── testdata │ ├── schema.go │ ├── parse.go │ ├── default.go │ └── datayml.go ├── divine │ ├── main │ │ └── main.go │ └── divine.go ├── rediskey │ └── rediskey.go └── http │ └── http.go ├── sqlboiler.toml ├── README.md ├── svc ├── app │ ├── redis.go │ ├── app.go │ ├── index.go │ ├── api.go │ ├── problem.go │ └── judge.go ├── redis │ ├── script.go │ ├── pipeline.go │ ├── scan.go │ ├── redis.go │ ├── timer.go │ └── x.go ├── email │ └── email.go ├── main │ ├── app │ │ └── main.go │ └── migrate │ │ └── main.go ├── migrate │ └── migrate.go └── judge │ └── judge.go ├── Dockerfile ├── .gitignore ├── .gitlab-ci.yml ├── models ├── boil_queries.go ├── mysql_suites_test.go ├── boil_queries_test.go ├── boil_table_names.go ├── mysql_upsert.go ├── boil_types.go ├── boil_main_test.go ├── mysql_main_test.go ├── boil_suites_test.go ├── file_test.go ├── user_test.go ├── article_test.go └── contest_test.go └── go.mod /lib/config/config.go: -------------------------------------------------------------------------------- 1 | // config gets application configuration and creates service instances from environment variables 2 | package config 3 | -------------------------------------------------------------------------------- /sqlboiler.toml: -------------------------------------------------------------------------------- 1 | [mysql] 2 | dbname = "syzoj" 3 | host = "172.21.0.11" 4 | port = "3306" 5 | user = "root" 6 | pass = "123456" 7 | sslmode = "false" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | syzoj-ng 2 | --- 3 | 4 | ## 许可证 5 | 本软件以 [GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/) 发布,见 [LICENSE](LICENSE). 6 | -------------------------------------------------------------------------------- /svc/app/redis.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | func (a *App) checkRedis(data interface{}, err error) { 4 | if err != nil { 5 | log.WithError(err).Warning("failure while executing redis command") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | WORKDIR /go/src/github.com/syzoj/syzoj-ng-go/ 3 | COPY . . 4 | RUN go get -v github.com/syzoj/syzoj-ng-go/svc/main/app && go get -v github.com/syzoj/syzoj-ng-go/svc/main/migrate 5 | 6 | FROM ubuntu 7 | WORKDIR /app 8 | COPY --from=0 /go/bin/ . 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | *.swp 15 | vendor 16 | -------------------------------------------------------------------------------- /lib/config/elastic.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/elastic/go-elasticsearch" 5 | "os" 6 | ) 7 | 8 | // Creates an elasticsearch client from environment variables. 9 | func NewElastic(prefix string) (*elasticsearch.Client, error) { 10 | return elasticsearch.NewClient(elasticsearch.Config{ 11 | Addresses: []string{os.Getenv(prefix + "ELASTICSEARCH")}, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/life/life.go: -------------------------------------------------------------------------------- 1 | package life 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | func SignalContext() context.Context { 11 | ch := make(chan os.Signal, 1) 12 | signal.Notify(ch, os.Interrupt) 13 | signal.Notify(ch, syscall.SIGTERM) 14 | ctx := context.Background() 15 | ctx, cancel := context.WithCancel(ctx) 16 | go func() { 17 | <-ch 18 | cancel() 19 | }() 20 | return ctx 21 | } 22 | -------------------------------------------------------------------------------- /lib/config/redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/gomodule/redigo/redis" 8 | ) 9 | 10 | // Creates a new *github.com/gomodule/redigo/redis.Pool instance from environment variables. 11 | // The environment variable is ${prefix}REDIS_ADDR, in host:port format. 12 | func NewRedis(prefix string) (*redis.Pool, error) { 13 | addr := os.Getenv(prefix + "REDIS_ADDR") 14 | pool := &redis.Pool{ 15 | MaxIdle: 3, 16 | IdleTimeout: 240 * time.Second, 17 | Dial: func() (redis.Conn, error) { return redis.Dial("tcp", addr) }, 18 | } 19 | return pool, nil 20 | } 21 | -------------------------------------------------------------------------------- /lib/config/email.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/syzoj/syzoj-ng-go/svc/email" 8 | ) 9 | 10 | func NewAliyunEmail(prefix string) (*email.EmailService, error) { 11 | accessKey := os.Getenv(prefix + "ACCESS_KEY_ID") 12 | secret := os.Getenv(prefix + "ACCESS_KEY_SECRET") 13 | accountName := os.Getenv(prefix + "ACCOUNT_NAME") 14 | if accessKey == "" || secret == "" || accountName == "" { 15 | return nil, fmt.Errorf("%sACCESS_KEY_ID or %sACCESS_KEY_SECRET or %sACCOUNT_NAME missing", prefix, prefix, prefix) 16 | } 17 | return email.DefaultEmailService(accessKey, secret, accountName), nil 18 | } 19 | -------------------------------------------------------------------------------- /lib/config/http.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var log = logrus.StandardLogger() 12 | 13 | // Get listen port for http server from environment variable HTTP_LISTEN_ADDR. 14 | func GetHttpListenAddr() string { 15 | return os.Getenv("HTTP_LISTEN_ADDR") 16 | } 17 | 18 | // Get URL for http endpoint from environment variable HTTP_URL. 19 | func GetHttpURL(prefix string) (*url.URL, error) { 20 | u := os.Getenv(prefix + "HTTP_URL") 21 | if u == "" { 22 | return nil, fmt.Errorf("Environment variable %sHTTP_URL not found", prefix) 23 | } 24 | return url.Parse(u) 25 | } 26 | -------------------------------------------------------------------------------- /lib/config/s3.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/minio/minio-go" 7 | ) 8 | 9 | // Creates an Amazon S3 compatible client from environment variables. 10 | // The environment variables are ${prefix}ENDPOINT, ${prefix}ACCESS_KEY, ${prefix}SECRET_KEY. 11 | // The prefix defaults to "S3_" if not specified. 12 | func NewMinio(prefix string) (*minio.Client, error) { 13 | if prefix == "" { 14 | prefix = "S3_" 15 | } 16 | endpoint := os.Getenv(prefix + "ENDPOINT") 17 | accessKey := os.Getenv(prefix + "ACCESS_KEY") 18 | secretKey := os.Getenv(prefix + "SECRET_KEY") 19 | return minio.New(endpoint, accessKey, secretKey, false) 20 | } 21 | -------------------------------------------------------------------------------- /lib/util/util.go: -------------------------------------------------------------------------------- 1 | // Utility library. 2 | package util 3 | 4 | import ( 5 | "crypto/rand" 6 | "encoding/hex" 7 | "time" 8 | ) 9 | 10 | // Creates a random hex string. n is the number of bytes. 11 | func RandomHex(n int) string { 12 | b := make([]byte, n) 13 | _, err := rand.Read(b) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return hex.EncodeToString(b) 18 | } 19 | 20 | // Allocates a string. 21 | func String(s string) *string { 22 | return &s 23 | } 24 | 25 | // Allocates a time.Time. 26 | func Time(t time.Time) *time.Time { 27 | return &t 28 | } 29 | 30 | // Allocates a *int64. 31 | func Int64(v int64) *int64 { 32 | return &v 33 | } 34 | 35 | // Allocates a *int. 36 | func Int(v int) *int { 37 | return &v 38 | } 39 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | docker-build-master: 3 | # Official docker image. 4 | image: docker:latest 5 | stage: build 6 | services: 7 | - docker:dind 8 | before_script: 9 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 10 | script: 11 | - docker build --pull -t "$CI_REGISTRY_IMAGE" . 12 | - docker push "$CI_REGISTRY_IMAGE" 13 | only: 14 | - master 15 | 16 | docker-build: 17 | # Official docker image. 18 | image: docker:latest 19 | stage: build 20 | services: 21 | - docker:dind 22 | before_script: 23 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY 24 | script: 25 | - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . 26 | - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" 27 | except: 28 | - master 29 | 30 | -------------------------------------------------------------------------------- /models/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "github.com/volatiletech/sqlboiler/drivers" 8 | "github.com/volatiletech/sqlboiler/queries" 9 | "github.com/volatiletech/sqlboiler/queries/qm" 10 | ) 11 | 12 | var dialect = drivers.Dialect{ 13 | LQ: 0x60, 14 | RQ: 0x60, 15 | 16 | UseIndexPlaceholders: false, 17 | UseLastInsertID: true, 18 | UseSchema: false, 19 | UseDefaultKeyword: false, 20 | UseAutoColumns: false, 21 | UseTopClause: false, 22 | UseOutputClause: false, 23 | UseCaseWhenExistsClause: false, 24 | } 25 | 26 | // NewQuery initializes a new Query using the passed in QueryMods 27 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 28 | q := &queries.Query{} 29 | queries.SetDialect(q, &dialect) 30 | qm.Apply(q, mods...) 31 | 32 | return q 33 | } 34 | -------------------------------------------------------------------------------- /lib/testdata/schema.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | type TestdataInfo struct { 4 | Cases map[string]*Testcase `json:"cases"` 5 | SpecialJudges map[string]*SpecialJudge `json:"special_judges"` 6 | Languages map[string]*Language `json:"languages"` 7 | Subtasks []*Subtask `json:"subtasks"` 8 | } 9 | 10 | type Testcase struct { 11 | MemoryLimit int64 `json:"memory_limit,omitempty"` 12 | TimeLimit int64 `json:"time_limit,omitempty"` 13 | Input *File `json:"input,omitempty"` 14 | Output *File `json:"output,omitempty"` 15 | Answer string `json:"answer,omitempty"` 16 | SpecialJudge string `json:"special_judge,omitempty"` 17 | } 18 | 19 | type SpecialJudge struct { 20 | Language string `json:"language,omitempty"` 21 | File *File `json:"file,omitempty"` 22 | } 23 | 24 | type Language struct { 25 | } 26 | 27 | type Subtask struct { 28 | Cases []string `json:"cases"` 29 | Score float64 `json:"score"` 30 | } 31 | 32 | type File struct { 33 | Name string `json:"name"` 34 | Sha256Sum string `json:"sha256sum"` 35 | } 36 | -------------------------------------------------------------------------------- /lib/testdata/parse.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func ParseTestdata(path string) (*TestdataInfo, error) { 12 | info, err := os.Stat(filepath.Join(path, "data.yml")) 13 | if err == nil && !info.IsDir() { 14 | return ParseDataYml(path) 15 | } 16 | return ParseDefault(path) 17 | } 18 | 19 | func getFile(path string, name string) (*File, error) { 20 | f, err := os.Open(filepath.Join(path, name)) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer f.Close() 25 | hasher := sha256.New() 26 | if _, err := io.Copy(hasher, f); err != nil { 27 | return nil, err 28 | } 29 | hash := hasher.Sum(nil) 30 | return &File{ 31 | Name: name, 32 | Sha256Sum: hex.EncodeToString(hash), 33 | }, nil 34 | } 35 | 36 | type fileSet map[string]*File 37 | 38 | func getFileCached(path string, name string, fs fileSet) (*File, error) { 39 | if f, ok := fs[name]; ok { 40 | return f, nil 41 | } 42 | f, err := getFile(path, name) 43 | if err == nil { 44 | fs[name] = f 45 | return f, nil 46 | } 47 | return nil, err 48 | } 49 | -------------------------------------------------------------------------------- /svc/redis/script.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | // Script wraps a Lua script to be used in Redis. 12 | type Script struct { 13 | Script []byte 14 | SHA1 [20]byte 15 | Hash string 16 | } 17 | 18 | func NewScript(sc []byte) *Script { 19 | sum := sha1.Sum(sc) 20 | return &Script{ 21 | Script: sc, 22 | SHA1: sum, 23 | Hash: hex.EncodeToString(sum[:]), 24 | } 25 | } 26 | 27 | // Evaluates a Lua script. 28 | func (r *RedisService) EvalContext(ctx context.Context, sc *Script, keys []string, data []interface{}) (interface{}, error) { 29 | args := make([]interface{}, 2+len(keys)+len(data)) 30 | args[0] = sc.Hash 31 | args[1] = len(keys) 32 | for i, key := range keys { 33 | args[i+2] = key 34 | } 35 | for i, dat := range data { 36 | args[i+2+len(keys)] = dat 37 | } 38 | val, err := r.DoContext(ctx, "EVALSHA", args...) 39 | if rerr, ok := err.(redis.Error); ok { 40 | if rerr[0:9] == "NOSCRIPT " { 41 | args[0] = sc.Script 42 | return r.DoContext(ctx, "EVAL", args...) 43 | } 44 | } 45 | return val, err 46 | } 47 | -------------------------------------------------------------------------------- /models/mysql_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | func TestUpsert(t *testing.T) { 9 | t.Run("Articles", testArticlesUpsert) 10 | 11 | t.Run("ArticleComments", testArticleCommentsUpsert) 12 | 13 | t.Run("Contests", testContestsUpsert) 14 | 15 | t.Run("ContestPlayers", testContestPlayersUpsert) 16 | 17 | t.Run("ContestRanklists", testContestRanklistsUpsert) 18 | 19 | t.Run("Files", testFilesUpsert) 20 | 21 | t.Run("FormattedCodes", testFormattedCodesUpsert) 22 | 23 | t.Run("JudgeStates", testJudgeStatesUpsert) 24 | 25 | t.Run("Problems", testProblemsUpsert) 26 | 27 | t.Run("ProblemTags", testProblemTagsUpsert) 28 | 29 | t.Run("ProblemTagMaps", testProblemTagMapsUpsert) 30 | 31 | t.Run("RatingCalculations", testRatingCalculationsUpsert) 32 | 33 | t.Run("RatingHistories", testRatingHistoriesUpsert) 34 | 35 | t.Run("SubmissionStatistics", testSubmissionStatisticsUpsert) 36 | 37 | t.Run("Users", testUsersUpsert) 38 | 39 | t.Run("UserPrivileges", testUserPrivilegesUpsert) 40 | } 41 | -------------------------------------------------------------------------------- /lib/divine/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/syzoj/syzoj-ng-go/lib/divine" 8 | ) 9 | 10 | type items struct { 11 | Items []*divine.Item `json:"items"` 12 | } 13 | 14 | var config = ` 15 | { 16 | "items": [ 17 | { 18 | "title": "刷题", 19 | "detail": [ 20 | "一遍过样例", 21 | null 22 | ] 23 | }, 24 | { 25 | "title": "装弱", 26 | "detail": [ 27 | "我好菜啊", 28 | "你太强了" 29 | ] 30 | }, 31 | { 32 | "title": { 33 | "boy": "搞基", 34 | "girl": "搞姬" 35 | }, 36 | "detail": [ 37 | "爱上学习", 38 | "会被掰弯" 39 | ] 40 | }, 41 | { 42 | "title": "直播写代码", 43 | "detail": [ 44 | "月入百万", 45 | "CE, RE and T,身败名裂" 46 | ] 47 | }, 48 | { 49 | "title": "学数论", 50 | "detail": [ 51 | "思维敏捷", 52 | "咋看都不会" 53 | ] 54 | }, 55 | { 56 | "title": "参加模拟赛", 57 | "detail": [ 58 | "AK 虐场", 59 | "爆零" 60 | ] 61 | } 62 | ] 63 | } 64 | ` 65 | 66 | func main() { 67 | var val items 68 | if err := json.Unmarshal([]byte(config), &val); err != nil { 69 | panic(err) 70 | } 71 | fmt.Printf("%#v\n", val.Items[0]) 72 | } 73 | -------------------------------------------------------------------------------- /svc/email/email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" 7 | "github.com/aliyun/alibaba-cloud-sdk-go/services/dm" 8 | ) 9 | 10 | type EmailService struct { 11 | cli *dm.Client 12 | AliyunAccountName string 13 | FromAlias string 14 | } 15 | 16 | func DefaultEmailService(aliyunAccessKeyId string, aliyunAccessKeySecret string, aliyunAccountName string) *EmailService { 17 | cli, err := dm.NewClientWithAccessKey("cn-hangzhou", aliyunAccessKeyId, aliyunAccessKeySecret) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return &EmailService{ 22 | cli: cli, 23 | AliyunAccountName: aliyunAccountName, 24 | FromAlias: "syzoj-ng", 25 | } 26 | } 27 | 28 | func (s *EmailService) SendEmail(ctx context.Context, To string, Subject string, Body string) error { 29 | req := dm.CreateSingleSendMailRequest() 30 | req.Scheme = "https" 31 | req.AccountName = s.AliyunAccountName 32 | req.AddressType = requests.NewInteger(1) 33 | req.ReplyToAddress = requests.NewBoolean(false) 34 | req.ToAddress = To 35 | req.FromAlias = s.FromAlias 36 | req.Subject = Subject 37 | req.HtmlBody = Body 38 | _, err := s.cli.SingleSendMail(req) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /models/boil_queries_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/rand" 12 | "regexp" 13 | 14 | "github.com/volatiletech/sqlboiler/boil" 15 | ) 16 | 17 | var dbNameRand *rand.Rand 18 | 19 | func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { 20 | if err != nil { 21 | panic(fmt.Sprintf("Cannot create a transactor: %s", err)) 22 | } 23 | return transactor 24 | } 25 | 26 | func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { 27 | return &fKeyDestroyer{ 28 | reader: reader, 29 | rgx: regex, 30 | } 31 | } 32 | 33 | type fKeyDestroyer struct { 34 | reader io.Reader 35 | buf *bytes.Buffer 36 | rgx *regexp.Regexp 37 | } 38 | 39 | func (f *fKeyDestroyer) Read(b []byte) (int, error) { 40 | if f.buf == nil { 41 | all, err := ioutil.ReadAll(f.reader) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) 47 | all = f.rgx.ReplaceAll(all, []byte{}) 48 | f.buf = bytes.NewBuffer(all) 49 | } 50 | 51 | return f.buf.Read(b) 52 | } 53 | -------------------------------------------------------------------------------- /models/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | Article string 8 | ArticleComment string 9 | Contest string 10 | ContestPlayer string 11 | ContestRanklist string 12 | File string 13 | FormattedCode string 14 | JudgeState string 15 | Problem string 16 | ProblemTag string 17 | ProblemTagMap string 18 | RatingCalculation string 19 | RatingHistory string 20 | SubmissionStatistics string 21 | User string 22 | UserPrivilege string 23 | }{ 24 | Article: "article", 25 | ArticleComment: "article_comment", 26 | Contest: "contest", 27 | ContestPlayer: "contest_player", 28 | ContestRanklist: "contest_ranklist", 29 | File: "file", 30 | FormattedCode: "formatted_code", 31 | JudgeState: "judge_state", 32 | Problem: "problem", 33 | ProblemTag: "problem_tag", 34 | ProblemTagMap: "problem_tag_map", 35 | RatingCalculation: "rating_calculation", 36 | RatingHistory: "rating_history", 37 | SubmissionStatistics: "submission_statistics", 38 | User: "user", 39 | UserPrivilege: "user_privilege", 40 | } 41 | -------------------------------------------------------------------------------- /lib/config/mysql.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | 7 | "github.com/go-sql-driver/mysql" 8 | "github.com/jmoiron/sqlx" 9 | ) 10 | 11 | // Creates a *sql.DB instance from environment variables. The environment variables are: 12 | // ${prefix}MYSQL_HOST, ${prefix}MYSQL_USER, ${prefix}MYSQL_PASSWORD, ${prefix}MYSQL_DATABASE. 13 | func NewMySQL(prefix string) (*sql.DB, error) { 14 | cfg := mysql.NewConfig() 15 | cfg.Net = "tcp" 16 | cfg.Addr = os.Getenv(prefix + "MYSQL_HOST") 17 | cfg.User = os.Getenv(prefix + "MYSQL_USER") 18 | cfg.Passwd = os.Getenv(prefix + "MYSQL_PASSWORD") 19 | cfg.DBName = os.Getenv(prefix + "MYSQL_DATABASE") 20 | cfg.ClientFoundRows = true // Default to return matched rows instead of changed rows in an UPDATE query since that makes more sense 21 | cfg.ParseTime = true 22 | return sql.Open("mysql", cfg.FormatDSN()) 23 | } 24 | 25 | // Creates a *sqlx.DB instance from environment variables. The environment variables are: 26 | // ${prefix}MYSQL_HOST, ${prefix}MYSQL_USER, ${prefix}MYSQL_PASSWORD, ${prefix}MYSQL_DATABASE. 27 | func NewMySQLx(prefix string) (*sqlx.DB, error) { 28 | cfg := mysql.NewConfig() 29 | cfg.Net = "tcp" 30 | cfg.Addr = os.Getenv(prefix + "MYSQL_HOST") 31 | cfg.User = os.Getenv(prefix + "MYSQL_USER") 32 | cfg.Passwd = os.Getenv(prefix + "MYSQL_PASSWORD") 33 | cfg.DBName = os.Getenv(prefix + "MYSQL_DATABASE") 34 | cfg.ClientFoundRows = true // Default to return matched rows instead of changed rows in an UPDATE query since that makes more sense 35 | cfg.ParseTime = true 36 | return sqlx.Open("mysql", cfg.FormatDSN()) 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syzoj/syzoj-ng-go 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect 7 | github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20191009062011-51fffd9a2436 8 | github.com/elastic/go-elasticsearch v0.0.0 9 | github.com/gin-contrib/sse v0.1.0 10 | github.com/gin-gonic/gin v1.4.0 11 | github.com/go-ini/ini v1.48.0 // indirect 12 | github.com/go-sql-driver/mysql v1.4.1 13 | github.com/gofrs/uuid v3.2.0+incompatible // indirect 14 | github.com/golang/protobuf v1.3.2 // indirect 15 | github.com/gomodule/redigo v2.0.0+incompatible 16 | github.com/gorilla/websocket v1.4.1 17 | github.com/jmoiron/sqlx v1.2.0 18 | github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 19 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 20 | github.com/lib/pq v1.1.1 // indirect 21 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 22 | github.com/microcosm-cc/bluemonday v1.0.2 23 | github.com/minio/minio-go v6.0.14+incompatible 24 | github.com/mitchellh/go-homedir v1.1.0 // indirect 25 | github.com/pkg/errors v0.8.1 26 | github.com/russross/blackfriday v2.0.0+incompatible 27 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 28 | github.com/sirupsen/logrus v1.4.2 29 | github.com/spf13/viper v1.4.0 30 | github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d // indirect 31 | github.com/volatiletech/null v8.0.0+incompatible 32 | github.com/volatiletech/sqlboiler v3.5.0+incompatible 33 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect 34 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect 35 | google.golang.org/appengine v1.6.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /svc/main/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/syzoj/syzoj-ng-go/lib/config" 8 | "github.com/syzoj/syzoj-ng-go/lib/life" 9 | "github.com/syzoj/syzoj-ng-go/svc/app" 10 | "github.com/syzoj/syzoj-ng-go/svc/judge" 11 | srvredis "github.com/syzoj/syzoj-ng-go/svc/redis" 12 | ) 13 | 14 | var log = logrus.StandardLogger() 15 | 16 | func main() { 17 | listenAddr := config.GetHttpListenAddr() 18 | 19 | redis, err := config.NewRedis("") 20 | if err != nil { 21 | log.WithError(err).Error("failed to get redis config") 22 | return 23 | } 24 | r := srvredis.DefaultRedisService(redis) 25 | 26 | redisCache, err := config.NewRedis("CACHE_") 27 | if err != nil { 28 | log.WithError(err).Error("failed to get redis cache config") 29 | return 30 | } 31 | rc := srvredis.DefaultRedisService(redisCache) 32 | 33 | judgeToken := os.Getenv("JUDGE_TOKEN") 34 | 35 | db, err := config.NewMySQL("") 36 | if err != nil { 37 | log.WithError(err).Error("failed to get mysql config") 38 | return 39 | } 40 | 41 | minio, err := config.NewMinio("") 42 | if err != nil { 43 | log.WithError(err).Error("failed to get s3 config") 44 | return 45 | } 46 | _, _ = r, minio 47 | 48 | judgeService := judge.DefaultJudgeService(db, r) 49 | 50 | emailService, _ := config.NewAliyunEmail("EMAIL_") 51 | if emailService == nil { 52 | log.Warning("Email service missing") 53 | } 54 | 55 | a := app.DefaultApp(db, r, rc, listenAddr, judgeService) 56 | a.JudgeToken = judgeToken 57 | a.EmailService = emailService 58 | //Minio: minio, 59 | //TestdataBucket: "testdata", 60 | a.Run(life.SignalContext()) 61 | } 62 | -------------------------------------------------------------------------------- /svc/main/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/syzoj/syzoj-ng-go/lib/config" 9 | "github.com/syzoj/syzoj-ng-go/lib/life" 10 | "github.com/syzoj/syzoj-ng-go/svc/migrate" 11 | srvredis "github.com/syzoj/syzoj-ng-go/svc/redis" 12 | ) 13 | 14 | var log = logrus.StandardLogger() 15 | var funcs = map[string]func(*migrate.MigrateService, context.Context) error{ 16 | "all": (*migrate.MigrateService).All, 17 | "problem-tags": (*migrate.MigrateService).MigrateProblemTags, 18 | "problem-counter": (*migrate.MigrateService).MigrateProblemCounter, 19 | "user-submissions": (*migrate.MigrateService).MigrateUserSubmissions, 20 | "user-email": (*migrate.MigrateService).MigrateUserEmail, 21 | } 22 | 23 | func main() { 24 | var flist []func(*migrate.MigrateService, context.Context) error 25 | for i, arg := range os.Args { 26 | if i == 0 { 27 | continue 28 | } 29 | if f, ok := funcs[arg]; ok { 30 | flist = append(flist, f) 31 | } else { 32 | log.WithField("arg", arg).Error("unsupported argument") 33 | } 34 | } 35 | redis, err := config.NewRedis("") 36 | if err != nil { 37 | log.WithError(err).Error("failed to get redis config") 38 | return 39 | } 40 | r := srvredis.DefaultRedisService(redis) 41 | db, err := config.NewMySQL("") 42 | if err != nil { 43 | log.WithError(err).Error("failed to get mysql config") 44 | return 45 | } 46 | srv := migrate.DefaultMigrateService(db, r) 47 | ctx := life.SignalContext() 48 | for _, f := range flist { 49 | if err := f(srv, ctx); err != nil { 50 | log.Error(err) 51 | return 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /models/mysql_upsert.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/volatiletech/sqlboiler/drivers" 11 | "github.com/volatiletech/sqlboiler/strmangle" 12 | ) 13 | 14 | // buildUpsertQueryMySQL builds a SQL statement string using the upsertData provided. 15 | func buildUpsertQueryMySQL(dia drivers.Dialect, tableName string, update, whitelist []string) string { 16 | whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) 17 | tableName = strmangle.IdentQuote(dia.LQ, dia.RQ, tableName) 18 | 19 | buf := strmangle.GetBuffer() 20 | defer strmangle.PutBuffer(buf) 21 | 22 | var columns string 23 | if len(whitelist) != 0 { 24 | columns = strings.Join(whitelist, ",") 25 | } 26 | 27 | if len(update) == 0 { 28 | fmt.Fprintf( 29 | buf, 30 | "INSERT IGNORE INTO %s (%s) VALUES (%s)", 31 | tableName, 32 | columns, 33 | strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1), 34 | ) 35 | return buf.String() 36 | } 37 | 38 | fmt.Fprintf( 39 | buf, 40 | "INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE ", 41 | tableName, 42 | columns, 43 | strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1), 44 | ) 45 | 46 | for i, v := range update { 47 | if i != 0 { 48 | buf.WriteByte(',') 49 | } 50 | quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) 51 | buf.WriteString(quoted) 52 | buf.WriteString(" = VALUES(") 53 | buf.WriteString(quoted) 54 | buf.WriteByte(')') 55 | } 56 | 57 | return buf.String() 58 | } 59 | -------------------------------------------------------------------------------- /lib/rediskey/rediskey.go: -------------------------------------------------------------------------------- 1 | package rediskey 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | type RedisKey []string 9 | 10 | func (k RedisKey) Format(s ...string) string { 11 | if len(s) != len(k)-1 { 12 | panic("RedisKey: Format: wrong number of arguments") 13 | } 14 | b := &strings.Builder{} 15 | b.WriteString(k[0]) 16 | for i, v := range s { 17 | for j := 0; j < len(v); j++ { 18 | if v[j] == ':' || v[j] == '{' || v[j] == '}' { 19 | panic("RedisKey: Format: key contains ':', '{', or '}'") 20 | } 21 | } 22 | b.WriteString(v) 23 | b.WriteString(k[i+1]) 24 | } 25 | return b.String() 26 | } 27 | 28 | var ( 29 | SESSION = RedisKey{"{session:", "}"} 30 | 31 | CORE_QUEUE = RedisKey{"{core:queue:", "}"} 32 | CORE_SUBMISSION_PROGRESS = RedisKey{"{core:submission:", "}:progress"} 33 | CORE_SUBMISSION_DATA = RedisKey{"{core:submission:", "}:data"} 34 | CORE_SUBMISSION_CALLBACK = RedisKey{"{core:submission:", "}:callback"} 35 | CORE_SUBMISSION_RESULT = RedisKey{"{core:submission:", "}:result"} 36 | 37 | MAIN_PROBLEM_SUBMITS = RedisKey{"{main:problem:", "}:submits"} 38 | MAIN_PROBLEM_ACCEPTS = RedisKey{"{main:problem:", "}:accepts"} 39 | MAIN_USER_LAST_SUBMISSION = RedisKey{"{main:user:", "}:last_submit"} 40 | MAIN_USER_LAST_ACCEPT = RedisKey{"{main:user:", "}:last_accept"} 41 | MAIN_JUDGE_DONE = "{main:judge_done}" 42 | MAIN_EMAIL_PASSWORD_RECOVERY_RATELIM = RedisKey{"{main:email:", "}:password_recovery_ratelim"} 43 | MAIN_EMAIL_PASSWORD_RECOVERY_TOKEN = RedisKey{"{main:email:", "}:password_recovery_token:", ""} 44 | ) 45 | 46 | // Default expiry time for keys that are no longer active. 47 | const DEFAULT_EXPIRE = time.Second * 86400 * 7 // a week 48 | -------------------------------------------------------------------------------- /lib/testdata/default.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | type defaultTestcase struct { 10 | Name string 11 | Input string 12 | Output string 13 | } 14 | 15 | func ParseDefault(path string) (*TestdataInfo, error) { 16 | files, err := ioutil.ReadDir(path) 17 | if err != nil { 18 | return nil, err 19 | } 20 | list := make(map[string]struct{}) 21 | for _, file := range files { 22 | if !file.IsDir() { 23 | list[file.Name()] = struct{}{} 24 | } 25 | } 26 | var cases []defaultTestcase 27 | for name := range list { 28 | if strings.HasSuffix(name, ".in") { 29 | caseName := name[:len(name)-3] 30 | var outFile string 31 | outFile1 := caseName + ".out" 32 | if _, exists := list[outFile1]; exists { 33 | outFile = outFile1 34 | } else { 35 | outFile2 := caseName + ".ans" 36 | if _, exists := list[outFile2]; exists { 37 | outFile = outFile2 38 | } 39 | } 40 | if outFile != "" { 41 | cases = append(cases, defaultTestcase{ 42 | Name: caseName, 43 | Input: caseName + ".in", 44 | Output: outFile, 45 | }) 46 | } 47 | } 48 | } 49 | if len(cases) == 0 { 50 | return nil, fmt.Errorf("cannot parse test data") 51 | } 52 | fset := make(fileSet) 53 | info := &TestdataInfo{} 54 | info.Cases = make(map[string]*Testcase, len(cases)) 55 | caseScore := 100. / float64(len(cases)) 56 | for _, testcase := range cases { 57 | inpFile, err := getFileCached(path, testcase.Input, fset) 58 | if err != nil { 59 | return nil, err 60 | } 61 | outFile, err := getFileCached(path, testcase.Output, fset) 62 | if err != nil { 63 | return nil, err 64 | } 65 | info.Cases[testcase.Name] = &Testcase{ 66 | Input: inpFile, 67 | Output: outFile, 68 | } 69 | info.Subtasks = append(info.Subtasks, &Subtask{ 70 | Cases: []string{testcase.Name}, 71 | Score: caseScore, 72 | }) 73 | } 74 | return info, nil 75 | } 76 | -------------------------------------------------------------------------------- /lib/http/http.go: -------------------------------------------------------------------------------- 1 | // http is a utility library for dealing with HTTP requests. 2 | package http 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "math/rand" 9 | ) 10 | 11 | // M is a shortcut for map[string]interface{}. 12 | type M map[string]interface{} 13 | 14 | // RetryPolicy describes a retry strategy. 15 | type RetryPolicy interface { 16 | // Keeps calling a function until the operation succeeds or retry limit exceeds. 17 | // It should return nil if the operation succeeds at least once. Otherwise, the 18 | // last error should be returned. 19 | Execute(context.Context, func() error) error 20 | } 21 | 22 | // ExpRetry tries the operation with an exponential backoff. 23 | type ExpRetry struct { 24 | MaxRetry int // Defaults to 5 25 | Unit time.Duration // Defaults to 500*time.Millisecond 26 | Cap time.Duration // Defaults to 15*time.Second 27 | Exp float64 // Defaults to 2 28 | Jitter float64 // Randomly varies the backoff to distribute requests evenly. Defaults to 0.4, must be [0,1) 29 | } 30 | 31 | // The default retry mechanism. 32 | var DefaultRetry = &ExpRetry{} 33 | 34 | // Implements the RetryPolicy interface. 35 | func (r *ExpRetry) Execute(ctx context.Context, f func() error) (err error) { 36 | cnt := r.MaxRetry 37 | if cnt < 0 { 38 | panic("ExpRetry: negative MaxRetry") 39 | } 40 | if cnt == 0 { 41 | cnt = 5 42 | } 43 | d := r.Unit 44 | if d == 0 { 45 | d = 500 * time.Millisecond 46 | } 47 | jitter := r.Jitter 48 | if jitter < 0 || jitter > 1 { 49 | panic("ExpRetry: invalid jitter") 50 | } 51 | if jitter == 0 { 52 | jitter = 0.4 53 | } 54 | exp := r.Exp 55 | if exp < 0 { 56 | panic("ExpRetry: invalid exp") 57 | } 58 | if exp == 0 { 59 | exp = 2 60 | } 61 | for ; cnt > 0; cnt-- { 62 | if err = f(); err == nil { 63 | return nil 64 | } 65 | select { 66 | case <-time.After(time.Duration(float64(d) * (1 - rand.Float64()*jitter))): 67 | case <-ctx.Done(): 68 | return ctx.Err() 69 | } 70 | d = time.Duration(float64(d) * exp) 71 | } 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /lib/divine/divine.go: -------------------------------------------------------------------------------- 1 | package divine 2 | 3 | import ( 4 | "hash/crc32" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type Divine struct { 10 | Fortune string `json:"fortune"` 11 | Good []*Item 12 | Bad []*Item 13 | } 14 | 15 | type Item struct { 16 | Title string `json:"title"` 17 | Detail string `json:"detail"` 18 | } 19 | 20 | type item struct { 21 | Title string 22 | Detail [2]string 23 | } 24 | 25 | var items = []*item{ 26 | {Title: "刷题", Detail: [2]string{"一遍过样例", ""}}, 27 | {Title: "装弱", Detail: [2]string{"我好菜啊", "你太强了"}}, 28 | {Title: "搞x", Detail: [2]string{"爱上学习", "会被掰弯"}}, 29 | {Title: "直播写代码", Detail: [2]string{"月入百万", "CE, RE and T, 身败名裂"}}, 30 | {Title: "学数论", Detail: [2]string{"思维敏捷", "咋看都不会"}}, 31 | {Title: "参加模拟赛", Detail: [2]string{"AK 虐场", "爆零"}}, 32 | } 33 | 34 | func DoDivine(name string, sex int) *Divine { 35 | res := &Divine{Good: []*Item{}, Bad: []*Item{}} 36 | seed := crc32.ChecksumIEEE([]byte(name + time.Now().Format("20060102"))) 37 | random := rand.New(rand.NewSource(int64(seed))) 38 | 39 | f := random.Float32() 40 | switch { 41 | case f <= 0.25: 42 | res.Fortune = "大吉" 43 | case f <= 0.5: 44 | res.Fortune = "大凶" 45 | case f <= 0.6: 46 | res.Fortune = "中平" 47 | case f <= 0.7: 48 | res.Fortune = "小吉" 49 | case f <= 0.8: 50 | res.Fortune = "小凶" 51 | case f <= 0.9: 52 | res.Fortune = "吉" 53 | default: 54 | res.Fortune = "凶" 55 | } 56 | 57 | pitems := make([]*item, len(items)) 58 | copy(pitems, items) 59 | makeItem := func(typ int) *Item { 60 | for { 61 | id := random.Intn(len(pitems)) 62 | item := pitems[id] 63 | if item == nil { 64 | continue 65 | } 66 | citem := &Item{Title: item.Title} 67 | if citem.Title == "搞x" { 68 | switch sex { 69 | case 0: 70 | citem.Title = "搞基" 71 | case 1: 72 | citem.Title = "搞姬" 73 | } 74 | } 75 | if item.Detail[typ] == "" { 76 | continue 77 | } else { 78 | citem.Detail = item.Detail[typ] 79 | } 80 | pitems[id] = nil 81 | return citem 82 | } 83 | } 84 | if res.Fortune != "大凶" { 85 | res.Good = []*Item{makeItem(0), makeItem(0)} 86 | } 87 | if res.Fortune != "大吉" { 88 | res.Bad = []*Item{makeItem(1), makeItem(1)} 89 | } 90 | return res 91 | } 92 | -------------------------------------------------------------------------------- /models/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/volatiletech/sqlboiler/boil" 11 | "github.com/volatiletech/sqlboiler/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | 54 | // Enum values for contest.type 55 | const ( 56 | ContestTypeNoi = "noi" 57 | ContestTypeIoi = "ioi" 58 | ContestTypeAcm = "acm" 59 | ) 60 | 61 | // Enum values for judge_state.status are not proper Go identifiers, cannot emit constants 62 | // Enum values for problem.type are not proper Go identifiers, cannot emit constants 63 | // Enum values for submission_statistics.type 64 | const ( 65 | SubmissionStatisticsTypeFastest = "fastest" 66 | SubmissionStatisticsTypeSlowest = "slowest" 67 | SubmissionStatisticsTypeShortest = "shortest" 68 | SubmissionStatisticsTypeLongest = "longest" 69 | SubmissionStatisticsTypeMin = "min" 70 | SubmissionStatisticsTypeMax = "max" 71 | SubmissionStatisticsTypeEarliest = "earliest" 72 | ) 73 | -------------------------------------------------------------------------------- /svc/redis/pipeline.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gomodule/redigo/redis" 8 | ) 9 | 10 | // Pipeline wraps a Redis connection to provide pipelining utilities. 11 | // It supports multiple concurrent callers. 12 | type Pipeline struct { 13 | conn redis.Conn 14 | mu sync.Mutex 15 | ch chan interface{} 16 | } 17 | 18 | // Create a new pipeline. The pipeline must be closed after use. 19 | func (r *RedisService) NewPipeline(ctx context.Context) (*Pipeline, error) { 20 | conn, err := r.Pool.GetContext(ctx) 21 | if err != nil { 22 | return nil, err 23 | } 24 | p := &Pipeline{conn: conn, ch: make(chan interface{}, 1000)} 25 | go p.run() 26 | return p, nil 27 | } 28 | 29 | // Add a command to pipeline. Note that callback will not receive connection errors, only redis errors. 30 | // Callback can be nil, in which case it will be ignored. 31 | // The callback will be called in order in a dedicated goroutine. 32 | func (p *Pipeline) Do(callback func(interface{}, error), cmdName string, args ...interface{}) { 33 | p.mu.Lock() 34 | p.conn.Send(cmdName, args...) 35 | p.ch <- callback 36 | p.mu.Unlock() 37 | } 38 | 39 | // Flush the pipeline and wait until all previous commands have returned. 40 | func (p *Pipeline) Flush(ctx context.Context) error { 41 | p.mu.Lock() 42 | if err := p.conn.Flush(); err != nil { 43 | p.mu.Unlock() 44 | return err 45 | } 46 | p.mu.Unlock() 47 | ch := make(chan error, 1) 48 | p.ch <- ch 49 | select { 50 | case <-ctx.Done(): 51 | return ctx.Err() 52 | case err := <-ch: 53 | return err 54 | } 55 | } 56 | 57 | func (p *Pipeline) run() { 58 | var perr error 59 | for f := range p.ch { 60 | switch obj := f.(type) { 61 | case chan error: // Flush command 62 | obj <- perr 63 | case func(interface{}, error): 64 | data, err := p.conn.Receive() 65 | if err != nil { 66 | if _, ok := err.(redis.Error); !ok { 67 | perr = err 68 | } 69 | } 70 | if perr == nil && obj != nil { 71 | obj(data, err) 72 | } 73 | } 74 | } 75 | } 76 | 77 | func (p *Pipeline) Close() error { 78 | close(p.ch) 79 | return p.conn.Close() 80 | } 81 | 82 | // A wrapper class to provide a callback that collects the results. 83 | type RedisResult struct { 84 | Result interface{} 85 | Err error 86 | } 87 | 88 | func (r *RedisResult) Save(a interface{}, e error) { 89 | r.Result = a 90 | r.Err = e 91 | } 92 | -------------------------------------------------------------------------------- /svc/redis/scan.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | type node struct { 12 | child map[string]*node 13 | scan func(string) 14 | } 15 | 16 | // Handles a Redis prefix. The prefix must either be empty or end in a colon. 17 | func (s *RedisService) ScanPrefix(prefix string, handler func(string)) { 18 | cur := &s.root 19 | for { 20 | ind := strings.Index(prefix, ":") 21 | if ind == -1 { 22 | break 23 | } 24 | name := prefix[:ind] 25 | prefix = prefix[ind+1:] 26 | if cur.scan != nil { 27 | panic("Scanner: conflicting prefix") 28 | } 29 | if cur.child == nil { 30 | cur.child = make(map[string]*node) 31 | } 32 | nex := &node{} 33 | cur.child[name] = nex 34 | cur = nex 35 | } 36 | if prefix != "" { 37 | panic("ScanPrefix: invalid prefix") 38 | } 39 | if cur.child != nil || cur.scan != nil { 40 | panic("ScanPrefix: conflicting prefix") 41 | } 42 | cur.scan = handler 43 | } 44 | 45 | func (r *RedisService) RunScanner(ctx context.Context) error { 46 | if r.Ratio < 0 || r.Ratio > 1 { 47 | panic("redis: invalid ratio") 48 | } 49 | r.cursor = 0 50 | for { 51 | select { 52 | case <-ctx.Done(): 53 | return ctx.Err() 54 | default: 55 | } 56 | startTime := time.Now() 57 | if err := r.loop(ctx); err != nil { 58 | r.ErrorHandler(err) 59 | } 60 | dur := time.Now().Sub(startTime) 61 | select { 62 | case <-ctx.Done(): 63 | return ctx.Err() 64 | case <-time.After(time.Duration(float64(dur) / r.Ratio)): 65 | } 66 | } 67 | } 68 | 69 | func (r *RedisService) loop(ctx context.Context) error { 70 | res, err := redis.Values(r.DoContext(ctx, "SCAN", r.cursor, "COUNT", r.BatchSize)) 71 | if err != nil { 72 | return err 73 | } 74 | r.cursor, err = redis.Int64(res[0], nil) 75 | if err != nil { 76 | return err 77 | } 78 | keys, err := redis.Strings(res[1], nil) 79 | if err != nil { 80 | return err 81 | } 82 | for _, key := range keys { 83 | cur := &r.root 84 | for cur.child != nil { 85 | ind := strings.Index(key, ":") 86 | name := key[:ind] 87 | if nex, ok := cur.child[name]; ok { 88 | cur = nex 89 | key = key[ind+1:] 90 | } else { 91 | cur = nil 92 | break 93 | } 94 | } 95 | if cur != nil && cur.scan != nil { 96 | cur.scan(key) 97 | } 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /svc/redis/redis.go: -------------------------------------------------------------------------------- 1 | // redis wraps around github.com/gomodule/redigo/redis to provide convenience functions. 2 | package redis 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var log = logrus.StandardLogger() 13 | 14 | // Redis service. 15 | type RedisService struct { 16 | *redis.Pool 17 | 18 | // For redis keyspace scanning 19 | BatchSize int 20 | Ratio float64 21 | ErrorHandler func(error) 22 | 23 | root node 24 | cursor int64 25 | } 26 | 27 | // Creates a Redis service with default settings. 28 | func DefaultRedisService(p *redis.Pool) *RedisService { 29 | return &RedisService{ 30 | Pool: p, 31 | BatchSize: 10, 32 | Ratio: 0.01, 33 | ErrorHandler: func(err error) { 34 | log.Error("error while scanning Redis: %s", err) 35 | }, 36 | } 37 | } 38 | 39 | // Do takes a connection from the pool and does the command. 40 | func (s *RedisService) Do(cmdName string, args ...interface{}) (interface{}, error) { 41 | conn := s.Get() 42 | defer conn.Close() 43 | return conn.Do(cmdName, args...) 44 | } 45 | 46 | // DoContext takes a connection from the pool and does the command. 47 | // Context deadline is respected. 48 | func (s *RedisService) DoContext(ctx context.Context, cmdName string, args ...interface{}) (interface{}, error) { 49 | conn, err := s.GetContext(ctx) 50 | if err != nil { 51 | return nil, err 52 | } 53 | defer conn.Close() 54 | conni := conn.(redis.ConnWithTimeout) 55 | deadline, ok := ctx.Deadline() 56 | var timeout time.Duration 57 | if ok { 58 | timeout = deadline.Sub(time.Now()) 59 | if timeout < 0 { 60 | return nil, ctx.Err() 61 | } 62 | } 63 | if timeout > 0 { 64 | return conni.DoWithTimeout(timeout, cmdName, args...) 65 | } else { 66 | return conni.Do(cmdName, args...) 67 | } 68 | } 69 | 70 | // WithCache queries redis for cached content before calling func. 71 | func (s *RedisService) WithCache(ctx context.Context, key string, f func() ([]byte, time.Duration, error)) ([]byte, error) { 72 | val, err := redis.Bytes(s.DoContext(ctx, "GET", key)) 73 | if err == redis.ErrNil { 74 | var d time.Duration 75 | val, d, err = f() 76 | if err != nil { 77 | return nil, err 78 | } 79 | _, err2 := s.DoContext(ctx, "SET", key, val, "PX", int64(d/time.Millisecond)) 80 | if err2 != nil { // this is not critical error 81 | log.WithError(err2).Warning("failed to write redis cache") 82 | } 83 | } 84 | return val, err 85 | } 86 | -------------------------------------------------------------------------------- /models/boil_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spf13/viper" 18 | "github.com/volatiletech/sqlboiler/boil" 19 | ) 20 | 21 | var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") 22 | var flagConfigFile = flag.String("test.config", "", "Overrides the default config") 23 | 24 | const outputDirDepth = 1 25 | 26 | var ( 27 | dbMain tester 28 | ) 29 | 30 | type tester interface { 31 | setup() error 32 | conn() (*sql.DB, error) 33 | teardown() error 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if dbMain == nil { 38 | fmt.Println("no dbMain tester interface was ready") 39 | os.Exit(-1) 40 | } 41 | 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | flag.Parse() 45 | 46 | var err error 47 | 48 | // Load configuration 49 | err = initViper() 50 | if err != nil { 51 | fmt.Println("unable to load config file") 52 | os.Exit(-2) 53 | } 54 | 55 | // Set DebugMode so we can see generated sql statements 56 | boil.DebugMode = *flagDebugMode 57 | 58 | if err = dbMain.setup(); err != nil { 59 | fmt.Println("Unable to execute setup:", err) 60 | os.Exit(-4) 61 | } 62 | 63 | conn, err := dbMain.conn() 64 | if err != nil { 65 | fmt.Println("failed to get connection:", err) 66 | } 67 | 68 | var code int 69 | boil.SetDB(conn) 70 | code = m.Run() 71 | 72 | if err = dbMain.teardown(); err != nil { 73 | fmt.Println("Unable to execute teardown:", err) 74 | os.Exit(-5) 75 | } 76 | 77 | os.Exit(code) 78 | } 79 | 80 | func initViper() error { 81 | if flagConfigFile != nil && *flagConfigFile != "" { 82 | viper.SetConfigFile(*flagConfigFile) 83 | if err := viper.ReadInConfig(); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | var err error 90 | 91 | viper.SetConfigName("sqlboiler") 92 | 93 | configHome := os.Getenv("XDG_CONFIG_HOME") 94 | homePath := os.Getenv("HOME") 95 | wd, err := os.Getwd() 96 | if err != nil { 97 | wd = strings.Repeat("../", outputDirDepth) 98 | } else { 99 | wd = wd + strings.Repeat("/..", outputDirDepth) 100 | } 101 | 102 | configPaths := []string{wd} 103 | if len(configHome) > 0 { 104 | configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) 105 | } else { 106 | configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) 107 | } 108 | 109 | for _, p := range configPaths { 110 | viper.AddConfigPath(p) 111 | } 112 | 113 | // Ignore errors here, fall back to defaults and validation to provide errs 114 | _ = viper.ReadInConfig() 115 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 116 | viper.AutomaticEnv() 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /lib/testdata/datayml.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-yaml/yaml" 10 | ) 11 | 12 | type dataYml struct { 13 | InputFile string `yaml:"inputFile"` 14 | OutputFile string `yaml:"outputFile"` 15 | UserOutput string `yaml:"userOutput"` 16 | Subtasks []*dataYmlSubtask `yaml:"subtasks"` 17 | SpecialJudge *dataYmlSpecialJudge `yaml:"specialJudge"` 18 | } 19 | 20 | type dataYmlSubtask struct { 21 | Score float64 `yaml:"score"` 22 | Type string `yaml:"type"` // sum, min, mul 23 | Cases []string `yaml:"cases"` 24 | } 25 | 26 | type dataYmlSpecialJudge struct { 27 | Language string `yaml:"language"` 28 | FileName string `yaml:"fileName"` 29 | } 30 | 31 | func ParseDataYml(path string) (*TestdataInfo, error) { 32 | filePath := filepath.Join(path, "data.yml") 33 | file, err := os.Open(filePath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer file.Close() 38 | fileReader := io.LimitReader(file, 100*1024) 39 | var data dataYml 40 | err = yaml.NewDecoder(fileReader).Decode(&data) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | fset := make(fileSet) 46 | info := &TestdataInfo{} 47 | info.SpecialJudges = make(map[string]*SpecialJudge) 48 | var spjName string 49 | if data.SpecialJudge != nil { 50 | spjName = "Spj" 51 | dataSpj := data.SpecialJudge 52 | spjFile, err := getFileCached(path, dataSpj.FileName, fset) 53 | if err != nil { 54 | return nil, err 55 | } 56 | info.SpecialJudges[spjName] = &SpecialJudge{ 57 | Language: dataSpj.Language, 58 | File: spjFile, 59 | } 60 | } 61 | info.Cases = make(map[string]*Testcase) 62 | for _, dataSubtask := range data.Subtasks { 63 | for _, dataCase := range dataSubtask.Cases { 64 | if _, exists := info.Cases[dataCase]; !exists { 65 | inpFile, err := getFileCached(path, strings.ReplaceAll(data.InputFile, "#", dataCase), fset) 66 | if err != nil { 67 | return nil, err 68 | } 69 | outFile, err := getFileCached(path, strings.ReplaceAll(data.OutputFile, "#", dataCase), fset) 70 | if err != nil { 71 | return nil, err 72 | } 73 | info.Cases[dataCase] = &Testcase{ 74 | Input: inpFile, 75 | Output: outFile, 76 | Answer: strings.ReplaceAll(data.UserOutput, "#", dataCase), 77 | SpecialJudge: spjName, 78 | } 79 | } 80 | } 81 | if dataSubtask.Type == "sum" && len(dataSubtask.Cases) != 0 { 82 | // Split into multiple subtasks 83 | score := dataSubtask.Score / float64(len(dataSubtask.Cases)) 84 | for _, dataCase := range dataSubtask.Cases { 85 | infoSubtask := &Subtask{} 86 | infoSubtask.Cases = []string{dataCase} 87 | infoSubtask.Score = score 88 | info.Subtasks = append(info.Subtasks, infoSubtask) 89 | } 90 | } else { 91 | infoSubtask := &Subtask{} 92 | infoSubtask.Cases = dataSubtask.Cases 93 | infoSubtask.Score = dataSubtask.Score 94 | info.Subtasks = append(info.Subtasks, infoSubtask) 95 | } 96 | } 97 | return info, nil 98 | } 99 | -------------------------------------------------------------------------------- /svc/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "crypto/subtle" 6 | "database/sql" 7 | "encoding/json" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/sirupsen/logrus" 13 | "github.com/syzoj/syzoj-ng-go/models" 14 | "github.com/syzoj/syzoj-ng-go/svc/email" 15 | "github.com/syzoj/syzoj-ng-go/svc/judge" 16 | svcredis "github.com/syzoj/syzoj-ng-go/svc/redis" 17 | "github.com/volatiletech/sqlboiler/queries/qm" 18 | ) 19 | 20 | var log = logrus.StandardLogger() 21 | 22 | const GIN_USER_ID = "USER_ID" 23 | 24 | type App struct { 25 | Db *sql.DB 26 | ListenAddr string 27 | Redis *svcredis.RedisService // The persistent redis instance. No eviction policies allowed. 28 | RedisCache *svcredis.RedisService 29 | JudgeService *judge.JudgeService 30 | EmailService *email.EmailService // optional 31 | JudgeToken string 32 | } 33 | 34 | func DefaultApp(db *sql.DB, redis *svcredis.RedisService, redisCache *svcredis.RedisService, listenAddr string, judgeService *judge.JudgeService) *App { 35 | return &App{ 36 | Db: db, 37 | ListenAddr: listenAddr, 38 | Redis: redis, 39 | RedisCache: redisCache, 40 | JudgeService: judgeService, 41 | } 42 | } 43 | 44 | func (a *App) Run(ctx context.Context) error { 45 | var wg sync.WaitGroup 46 | wg.Add(1) 47 | go func() { 48 | defer wg.Done() 49 | if err := a.ensureQueue(ctx, "default"); err != nil { 50 | log.WithError(err).Error("failed to create default queue") 51 | } 52 | }() 53 | router := gin.Default() 54 | router.Use(a.UserMiddleware) 55 | router.GET("/api/index", a.getApiIndex) 56 | router.POST("/api/login", a.postApiLogin) 57 | router.POST("/api/forget", a.postApiForget) 58 | router.GET("/api/problems", a.getApiProblems) 59 | router.GET("/api/problem/:problem_id", a.getApiProblem) 60 | router.GET("/api/submission-progress/:sid", a.getTaskProgress) 61 | router.GET("/api/header", a.getHeader) 62 | jg := router.Group("/judge") 63 | jg.Use(a.useCheckJudgeToken) 64 | jg.GET("/wait-for-task", a.getJudgeWaitForTask) 65 | server := &http.Server{Addr: a.ListenAddr, Handler: router} 66 | wg.Add(1) 67 | go func() { 68 | defer wg.Done() 69 | <-ctx.Done() 70 | server.Close() 71 | }() 72 | wg.Add(1) 73 | go func() { 74 | defer wg.Done() 75 | if err := server.ListenAndServe(); err != nil { 76 | log.WithError(err).Error("failed to listen and serve") 77 | } 78 | }() 79 | go func() { 80 | defer wg.Done() 81 | if err := a.handleJudgeDone(ctx); err != nil { 82 | log.WithError(err).Error("failed to handle judge done") 83 | } 84 | }() 85 | wg.Wait() 86 | return nil 87 | } 88 | 89 | func (a *App) UserMiddleware(c *gin.Context) { 90 | ctx := c.Request.Context() 91 | c.Set(GIN_USER_ID, 0) 92 | loginCookie, err := c.Cookie("login") 93 | if err != nil { 94 | return 95 | } 96 | var loginData []string 97 | if err := json.Unmarshal([]byte(loginCookie), &loginData); err != nil { 98 | return 99 | } 100 | if len(loginData) != 2 { 101 | return 102 | } 103 | user, err := models.Users(qm.Select("id", "password"), qm.Where("username=?", loginData[0])).One(ctx, a.Db) 104 | if err != nil { 105 | return 106 | } 107 | if !user.Password.Valid || subtle.ConstantTimeCompare([]byte(loginData[1]), []byte(user.Password.String)) != 1 { 108 | return 109 | } 110 | c.Set(GIN_USER_ID, user.ID) 111 | } 112 | -------------------------------------------------------------------------------- /svc/app/index.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/syzoj/syzoj-ng-go/lib/divine" 8 | "github.com/syzoj/syzoj-ng-go/models" 9 | "github.com/volatiletech/null" 10 | "github.com/volatiletech/sqlboiler/queries/qm" 11 | ) 12 | 13 | type IndexResponse struct { 14 | Divine *divine.Divine 15 | Ranklist []*IndexResponseUser `json:"ranklist"` 16 | Notices []*IndexResponseNotice `json:"notices"` 17 | Contests []*IndexResponseContest `json:"contests"` 18 | Problems []*IndexResponseProblem `json:"problems"` 19 | } 20 | type IndexResponseUser struct { 21 | ID int `json:"id"` 22 | Username null.String `json:"username"` 23 | Nameplate null.String `json:"nameplate"` 24 | Information null.String `json:"information"` 25 | } 26 | type IndexResponseContest struct { 27 | ID int `json:"id"` 28 | StartTime null.Int `json:"start_time"` 29 | EndTime null.Int `json:"end_time"` 30 | } 31 | type IndexResponseNotice struct { 32 | ID int `json:"id"` 33 | Title null.String `json:"title"` 34 | Date null.Int `json:"date"` 35 | } 36 | type IndexResponseProblem struct { 37 | ID int `json:"id"` 38 | Title null.String `json:"title"` 39 | PublicizeTime null.Time `json:"publicize_time"` 40 | } 41 | 42 | func (a *App) getApiIndex(c *gin.Context) { 43 | ctx := c.Request.Context() 44 | users, err := models.Users(qm.OrderBy("rating desc"), qm.Limit(20)).All(ctx, a.Db) 45 | if err != nil { 46 | c.AbortWithError(500, err) 47 | return 48 | } 49 | articles, err := models.Articles(qm.Where("is_notice = true"), qm.OrderBy("public_time desc")).All(ctx, a.Db) 50 | if err != nil { 51 | c.AbortWithError(500, err) 52 | return 53 | } 54 | contests, err := models.Contests(qm.Where("is_public = true"), qm.OrderBy("start_time desc"), qm.Limit(5)).All(ctx, a.Db) 55 | if err != nil { 56 | c.AbortWithError(500, err) 57 | return 58 | } 59 | problems, err := models.Problems(qm.Where("is_public = true"), qm.OrderBy("publicize_time desc"), qm.Limit(5)).All(ctx, a.Db) 60 | if err != nil { 61 | c.AbortWithError(500, err) 62 | return 63 | } 64 | resp := &IndexResponse{ 65 | Ranklist: []*IndexResponseUser{}, 66 | Notices: []*IndexResponseNotice{}, 67 | Contests: []*IndexResponseContest{}, 68 | Problems: []*IndexResponseProblem{}, 69 | } 70 | for _, user := range users { 71 | resp.Ranklist = append(resp.Ranklist, &IndexResponseUser{ 72 | ID: user.ID, 73 | Username: user.Username, 74 | Nameplate: user.Nameplate, 75 | Information: user.Information, 76 | }) 77 | } 78 | for _, article := range articles { 79 | resp.Notices = append(resp.Notices, &IndexResponseNotice{ 80 | ID: article.ID, 81 | Title: article.Title, 82 | Date: article.PublicTime, 83 | }) 84 | } 85 | for _, contest := range contests { 86 | resp.Contests = append(resp.Contests, &IndexResponseContest{ 87 | ID: contest.ID, 88 | StartTime: contest.StartTime, 89 | EndTime: contest.EndTime, 90 | }) 91 | _ = contest 92 | } 93 | for _, problem := range problems { 94 | resp.Problems = append(resp.Problems, &IndexResponseProblem{ 95 | ID: problem.ID, 96 | Title: problem.Title, 97 | PublicizeTime: problem.PublicizeTime, 98 | }) 99 | } 100 | /* TODO: fortune */ 101 | userId := c.GetInt(GIN_USER_ID) 102 | if userId != 0 { 103 | // TODO: supply sex 104 | resp.Divine = divine.DoDivine(strconv.Itoa(userId), 0) 105 | } 106 | c.JSON(200, resp) 107 | } 108 | -------------------------------------------------------------------------------- /svc/redis/timer.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | // Redis timer is designed around a ZSET. The score is used as Unix time in milliseconds. 12 | // It polls a Redis ZSET to fetch latest messages. Instead of popping items out of ZSET, 13 | // it modifies the score to some time in the future so items can't be lost. The item 14 | // will be deleted automatically after the associated work is done. 15 | type RedisTimer struct { 16 | r *RedisService 17 | Key string 18 | Handler func(context.Context, string) error 19 | Timeout time.Duration 20 | Interval time.Duration // Maximum interval between polls 21 | BatchSize int 22 | 23 | tmr *time.Timer 24 | tmrmu sync.Mutex 25 | nextRun time.Time 26 | } 27 | 28 | func (r *RedisService) DefaultTimer(key string, handler func(context.Context, string) error) *RedisTimer { 29 | return &RedisTimer{ 30 | r: r, 31 | Key: key, 32 | Handler: handler, 33 | Timeout: time.Second * 60, 34 | Interval: time.Second * 5, 35 | BatchSize: 10, 36 | tmr: time.NewTimer(0), 37 | } 38 | } 39 | 40 | func TimeToMillisecond(t time.Time) int64 { 41 | return t.Unix()*1000 + int64(t.Nanosecond())/1000000 42 | } 43 | 44 | func TimeFromMillisecond(t int64) time.Time { 45 | return time.Unix(t/1000, (t%1000)*1000000) 46 | } 47 | 48 | // Fetch a few elements, set their score to a new value, then find the lowest score. 49 | var timerFetchScript = NewScript([]byte(` 50 | local keys = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1]) 51 | local args = {} 52 | for i,key in pairs(keys) do 53 | args[#args+1] = ARGV[2] 54 | args[#args+1] = key 55 | end 56 | if next(args) ~= nil then redis.call("ZADD", KEYS[1], unpack(args)) end 57 | local first = redis.call("ZRANGE", KEYS[1], 0, 0, "WITHSCORES") 58 | if next(first) == nil then first = false else first = first[2] end 59 | return { first, keys } 60 | `)) 61 | 62 | func (t *RedisTimer) Run(ctx context.Context) error { 63 | t.reschedule(time.Now()) 64 | for { 65 | select { 66 | case <-ctx.Done(): 67 | return ctx.Err() 68 | case <-t.tmr.C: 69 | } 70 | now := time.Now() 71 | nex := now.Add(t.Timeout) 72 | res, err := redis.Values(t.r.EvalContext(ctx, timerFetchScript, []string{t.Key}, []interface{}{TimeToMillisecond(now), TimeToMillisecond(nex)})) 73 | if err != nil { 74 | log.WithField("key", t.Key).WithError(err).Errorf("failed to fetch redis timer") 75 | t.reschedule(time.Now().Add(t.Interval)) 76 | continue 77 | } 78 | keys, err := redis.Strings(res[1], nil) 79 | if err != nil { 80 | panic(err) 81 | } 82 | for _, key := range keys { 83 | if err := t.Handler(ctx, key); err != nil { 84 | log.WithField("key", t.Key).WithError(err).Error("failed to handle redis timer") 85 | } else if err = t.Delete(ctx, key); err != nil { 86 | log.WithField("key", t.Key).WithError(err).Error("failed to drain redis timer") 87 | } 88 | select { 89 | case <-ctx.Done(): 90 | break 91 | default: 92 | } 93 | } 94 | nexTime := time.Now().Add(t.Interval) 95 | firstTime, err := redis.Int64(res[0], nil) 96 | if err == nil { 97 | ft := TimeFromMillisecond(firstTime) 98 | if ft.Before(nexTime) { 99 | nexTime = ft 100 | } 101 | } else if err != redis.ErrNil { 102 | panic(err) 103 | } 104 | t.reschedule(nexTime) 105 | } 106 | } 107 | 108 | func (t *RedisTimer) reschedule(v time.Time) { 109 | t.tmrmu.Lock() 110 | defer t.tmrmu.Unlock() 111 | d := v.Sub(time.Now()) 112 | if d < 0 { 113 | d = 0 114 | } 115 | t.tmr.Reset(d) 116 | t.nextRun = v 117 | } 118 | 119 | func (t *RedisTimer) schedule(v time.Time) { 120 | t.tmrmu.Lock() 121 | defer t.tmrmu.Unlock() 122 | if v.After(t.nextRun) { 123 | return 124 | } 125 | d := v.Sub(time.Now()) 126 | if d < 0 { 127 | d = 0 128 | } 129 | t.tmr.Reset(d) 130 | t.nextRun = v 131 | } 132 | 133 | // Schedule key at the specified time. If key already exists, it is rescheduled. 134 | func (t *RedisTimer) Schedule(ctx context.Context, key string, at time.Time) error { 135 | t.schedule(at) 136 | _, err := t.r.DoContext(ctx, "ZADD", t.Key, TimeToMillisecond(at), key) 137 | return err 138 | } 139 | 140 | // Delete key. 141 | func (t *RedisTimer) Delete(ctx context.Context, key string) error { 142 | _, err := t.r.DoContext(ctx, "ZREM", t.Key, key) 143 | return err 144 | } 145 | -------------------------------------------------------------------------------- /svc/app/api.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/subtle" 7 | "database/sql" 8 | "encoding/base64" 9 | "encoding/json" 10 | "html/template" 11 | "net/url" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/gomodule/redigo/redis" 15 | "github.com/syzoj/syzoj-ng-go/lib/rediskey" 16 | "github.com/syzoj/syzoj-ng-go/models" 17 | "github.com/volatiletech/sqlboiler/queries/qm" 18 | ) 19 | 20 | type HeaderResponse struct { 21 | User *models.User `json:"user"` 22 | } 23 | 24 | func (a *App) getHeader(c *gin.Context) { 25 | ctx := c.Request.Context() 26 | resp := &HeaderResponse{} 27 | userId := c.GetInt(GIN_USER_ID) 28 | if userId != 0 { 29 | user, err := models.Users(qm.Select("username", "is_admin"), qm.Where("id=?", userId)).One(ctx, a.Db) 30 | if err != nil { 31 | c.AbortWithError(500, err) 32 | return 33 | } 34 | resp.User = &models.User{ 35 | Username: user.Username, 36 | IsAdmin: user.IsAdmin, 37 | } 38 | } 39 | c.JSON(200, resp) 40 | } 41 | 42 | type LoginRequest struct { 43 | UserName string `json:"username" form:"username"` 44 | Password string `json:"password" form:"password"` 45 | } 46 | 47 | func (a *App) postApiLogin(c *gin.Context) { 48 | ctx := c.Request.Context() 49 | var req LoginRequest 50 | if err := c.Bind(&req); err != nil { 51 | c.AbortWithError(400, err) 52 | return 53 | } 54 | 55 | user, err := models.Users(qm.Where("username=?", req.UserName)).One(ctx, a.Db) 56 | if err == sql.ErrNoRows { 57 | c.JSON(200, gin.H{"error_code": 1001}) 58 | return 59 | } else if err != nil { 60 | c.AbortWithError(500, err) 61 | return 62 | } 63 | if !user.Password.Valid || user.Password.String == "" { 64 | c.JSON(200, gin.H{"error_code": 1003}) 65 | return 66 | } 67 | if subtle.ConstantTimeCompare([]byte(user.Password.String), []byte(req.Password)) == 0 { 68 | c.JSON(200, gin.H{"error_code": 1002}) 69 | return 70 | } 71 | data, _ := json.Marshal([]string{req.UserName, req.Password}) 72 | c.SetCookie("login", string(data), 86400*31, "/", "", false, true) 73 | c.JSON(200, gin.H{"error_code": 1}) 74 | } 75 | 76 | type ForgetRequest struct { 77 | Email string `json:"email" form:"email"` 78 | } 79 | 80 | type forgetData struct { 81 | Username string 82 | Url string 83 | } 84 | 85 | var forgetTitleTemplate = template.Must(template.New("forget").Parse("{{.Username}} 的密码重置邮件")) 86 | var forgetBodyTemplate = template.Must(template.New("forget").Parse(`

请点击该链接来重置密码:

{{.Url}}

链接有效期为 12h。如果您不是 {{.Username}},请忽略此邮件。

`)) 87 | 88 | func (a *App) postApiForget(c *gin.Context) { 89 | if a.EmailService == nil { 90 | c.JSON(500, gin.H{"error": "Email service not configured"}) 91 | return 92 | } 93 | ctx := c.Request.Context() 94 | var req ForgetRequest 95 | if err := c.Bind(&req); err != nil { 96 | c.AbortWithError(400, err) 97 | return 98 | } 99 | user, err := models.Users(qm.Where("email=?", req.Email)).One(ctx, a.Db) 100 | if err == sql.ErrNoRows { 101 | c.JSON(200, gin.H{"error_code": 1001}) 102 | return 103 | } else if err != nil { 104 | c.AbortWithError(500, err) 105 | return 106 | } 107 | 108 | _, err = a.RedisCache.DoContext(ctx, "SET", rediskey.MAIN_EMAIL_PASSWORD_RECOVERY_RATELIM.Format(req.Email), "", "EX", 3600, "NX") 109 | if err == redis.ErrNil { 110 | c.JSON(200, gin.H{"error": "Only one email allowed per hour"}) 111 | return 112 | } else if err != nil { 113 | c.AbortWithError(500, err) 114 | return 115 | } 116 | 117 | token := make([]byte, 32) 118 | if _, err := rand.Read(token); err != nil { 119 | c.AbortWithError(500, err) 120 | return 121 | } 122 | tokenString := base64.URLEncoding.EncodeToString(token) 123 | _, err = a.RedisCache.DoContext(ctx, "SET", rediskey.MAIN_EMAIL_PASSWORD_RECOVERY_TOKEN.Format(req.Email, tokenString), "", "EX", 43200) 124 | if err != nil { 125 | c.AbortWithError(500, err) 126 | return 127 | } 128 | forgetData := &forgetData{Username: user.Username.String, Url: "/forget_confirm?email=" + url.QueryEscape(req.Email) + "&token=" + tokenString} 129 | bufTitle := &bytes.Buffer{} 130 | if err := forgetTitleTemplate.Execute(bufTitle, forgetData); err != nil { 131 | c.AbortWithError(500, err) 132 | return 133 | } 134 | bufBody := &bytes.Buffer{} 135 | if err := forgetBodyTemplate.Execute(bufBody, forgetData); err != nil { 136 | c.AbortWithError(500, err) 137 | return 138 | } 139 | 140 | err = a.EmailService.SendEmail(ctx, req.Email, bufTitle.String(), bufBody.String()) 141 | if err != nil { 142 | c.AbortWithError(500, err) 143 | return 144 | } 145 | c.JSON(200, gin.H{"error_code": 1}) 146 | } 147 | -------------------------------------------------------------------------------- /svc/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "strconv" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/syzoj/syzoj-ng-go/lib/rediskey" 11 | "github.com/syzoj/syzoj-ng-go/models" 12 | "github.com/syzoj/syzoj-ng-go/svc/redis" 13 | "github.com/volatiletech/null" 14 | "github.com/volatiletech/sqlboiler/queries" 15 | "github.com/volatiletech/sqlboiler/queries/qm" 16 | ) 17 | 18 | var log = logrus.StandardLogger() 19 | 20 | type MigrateService struct { 21 | Db *sql.DB 22 | Redis *redis.RedisService 23 | } 24 | 25 | func DefaultMigrateService(db *sql.DB, redis *redis.RedisService) *MigrateService { 26 | return &MigrateService{ 27 | Db: db, 28 | Redis: redis, 29 | } 30 | } 31 | 32 | func (m *MigrateService) MigrateProblemTags(ctx context.Context) error { 33 | log.Info("Computing problem tags") 34 | m.Db.ExecContext(ctx, "ALTER TABLE `problem` ADD COLUMN `tags` JSON DEFAULT NULL") 35 | tagMaps, err := models.ProblemTagMaps().All(ctx, m.Db) 36 | if err != nil { 37 | return err 38 | } 39 | tags, err := models.ProblemTags().All(ctx, m.Db) 40 | if err != nil { 41 | return err 42 | } 43 | tagsMap := make(map[int]string) 44 | for _, tag := range tags { 45 | if tag.Name.Valid { 46 | tagsMap[tag.ID] = tag.Name.String 47 | } 48 | } 49 | problemTags := make(map[int][]string) 50 | for _, tagMap := range tagMaps { 51 | pid := tagMap.ProblemID 52 | tid := tagMap.TagID 53 | problemTags[pid] = append(problemTags[pid], tagsMap[tid]) 54 | } 55 | for pid, tags := range problemTags { 56 | b, err := json.Marshal(tags) 57 | if err != nil { 58 | return err 59 | } 60 | if _, err := m.Db.ExecContext(ctx, "UPDATE `problem` SET `tags`=? WHERE `id`=?", b, pid); err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (m *MigrateService) MigrateProblemCounter(ctx context.Context) error { 68 | log.Info("Creating problem counters") 69 | problems, err := models.Problems().All(ctx, m.Db) 70 | if err != nil { 71 | return err 72 | } 73 | pipeline, err := m.Redis.NewPipeline(ctx) 74 | if err != nil { 75 | return err 76 | } 77 | for _, problem := range problems { 78 | if problem.AcNum.Valid { 79 | pipeline.Do(nil, "SET", rediskey.MAIN_PROBLEM_ACCEPTS.Format(strconv.Itoa(problem.ID)), problem.AcNum.Int) 80 | } 81 | if problem.SubmitNum.Valid { 82 | pipeline.Do(nil, "SET", rediskey.MAIN_PROBLEM_SUBMITS.Format(strconv.Itoa(problem.ID)), problem.SubmitNum.Int) 83 | } 84 | } 85 | return pipeline.Flush(ctx) 86 | } 87 | 88 | func (m *MigrateService) MigrateUserSubmissions(ctx context.Context) error { 89 | log.Info("Migrating user submissions") 90 | query, err := models.JudgeStates(qm.Select("status", "user_id", "problem_id"), qm.OrderBy("submit_time")).QueryContext(ctx, m.Db) 91 | if err != nil { 92 | return err 93 | } 94 | pipeline, err := m.Redis.NewPipeline(ctx) 95 | if err != nil { 96 | return err 97 | } 98 | for query.Next() { 99 | submission := &models.JudgeState{} 100 | if err := queries.Bind(query, submission); err != nil { 101 | log.WithError(err).Error("failed to bind model") 102 | continue 103 | } 104 | if !submission.UserID.Valid || !submission.ProblemID.Valid { 105 | continue 106 | } 107 | userId := submission.UserID.Int 108 | problemId := submission.ProblemID.Int 109 | if submission.Status == null.StringFrom("Accepted") { 110 | pipeline.Do(nil, "HSET", rediskey.MAIN_USER_LAST_ACCEPT.Format(strconv.Itoa(userId)), problemId, submission.ID) 111 | } 112 | pipeline.Do(nil, "HSET", rediskey.MAIN_USER_LAST_SUBMISSION.Format(strconv.Itoa(userId)), problemId, submission.ID) 113 | } 114 | if err := query.Err(); err != nil { 115 | return err 116 | } 117 | return pipeline.Flush(ctx) 118 | } 119 | 120 | func (m *MigrateService) MigrateUserEmail(ctx context.Context) error { 121 | log.Info("Adding user email verify status column") 122 | _, err := m.Db.ExecContext(ctx, "ALTER TABLE `user` ADD COLUMN `email_verified` TINYINT NOT NULL DEFAULT 0") 123 | rows, err := m.Db.QueryContext(ctx, "SELECT `email_verified` FROM `user` LIMIT 1") 124 | if err != nil { 125 | return err 126 | } 127 | rows.Close() 128 | return nil 129 | } 130 | 131 | func (m *MigrateService) All(ctx context.Context) error { 132 | if err := m.MigrateProblemTags(ctx); err != nil { 133 | return err 134 | } 135 | if err := m.MigrateProblemCounter(ctx); err != nil { 136 | return err 137 | } 138 | if err := m.MigrateUserSubmissions(ctx); err != nil { 139 | return err 140 | } 141 | if err := m.MigrateUserEmail(ctx); err != nil { 142 | return err 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /svc/redis/x.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gomodule/redigo/redis" 7 | ) 8 | 9 | type XREAD struct { 10 | Key string 11 | Msgs []*XREADMsg 12 | } 13 | 14 | type XREADMsg struct { 15 | ID string 16 | Data map[string]string 17 | } 18 | 19 | // Parse XREAD and XREADGROUP result. 20 | func ParseXREAD(data interface{}, err error) ([]*XREAD, error) { 21 | if err != nil { 22 | return nil, err 23 | } 24 | if data == nil { 25 | return nil, redis.ErrNil 26 | } 27 | vals := data.([]interface{}) 28 | res := make([]*XREAD, len(vals)) 29 | for i, val := range vals { 30 | valv := val.([]interface{}) 31 | xread := &XREAD{} 32 | if xread.Key, err = redis.String(valv[0], nil); err != nil { 33 | return nil, err 34 | } 35 | msgs := valv[1].([]interface{}) 36 | xread.Msgs = make([]*XREADMsg, len(msgs)) 37 | for j, msg := range msgs { 38 | msgv := msg.([]interface{}) 39 | xmsg := &XREADMsg{} 40 | if xmsg.ID, err = redis.String(msgv[0], nil); err != nil { 41 | return nil, err 42 | } 43 | kvs, err := redis.Values(msgv[1], nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | xmsg.Data = make(map[string]string, len(kvs)/2) 48 | for i := 0; i < len(kvs); i += 2 { 49 | k, err := redis.String(kvs[i], nil) 50 | if err != nil { 51 | return nil, err 52 | } 53 | v, err := redis.String(kvs[i+1], nil) 54 | if err != nil { 55 | return nil, err 56 | } 57 | xmsg.Data[k] = v 58 | } 59 | xread.Msgs[j] = xmsg 60 | } 61 | res[i] = xread 62 | } 63 | return res, nil 64 | } 65 | 66 | // Read up to entries from a Redis 5 stream until context is done. 67 | // If count==0, it reads as many entries as possible. 68 | func (r *RedisService) ReadStreamOnce(ctx context.Context, key string, lastId string, count int) ([]*XREADMsg, error) { 69 | for { 70 | select { 71 | case <-ctx.Done(): 72 | return nil, ctx.Err() 73 | default: 74 | } 75 | var ( 76 | data []*XREAD 77 | err error 78 | ) 79 | if count == 0 { 80 | data, err = ParseXREAD(r.DoContext(ctx, "XREAD", "BLOCK", 60000, "STREAMS", key, lastId)) 81 | } else { 82 | data, err = ParseXREAD(r.DoContext(ctx, "XREAD", "COUNT", count, "BLOCK", 60000, "STREAMS", key, lastId)) 83 | } 84 | if err == redis.ErrNil { 85 | continue 86 | } 87 | return data[0].Msgs, err 88 | } 89 | } 90 | 91 | // Continuously reads from a single Redis 5 stream until context is done. 92 | // When the operation completes, an error is returned, and both channels will be closed. 93 | // Be sure to read from the returned channels or the stream will block. 94 | // Set lastId to "0" to read from beginning, or "$" to read new entries (racy). 95 | func (r *RedisService) ReadStream(ctx context.Context, key string, lastId string) (<-chan *XREADMsg, <-chan error) { 96 | msgCh := make(chan *XREADMsg) 97 | msgErr := make(chan error, 1) 98 | go func() { 99 | var err error 100 | defer func() { 101 | msgErr <- err 102 | close(msgCh) 103 | close(msgErr) 104 | }() 105 | for { 106 | var data []*XREADMsg 107 | data, err = r.ReadStreamOnce(ctx, key, lastId, 0) 108 | if err != nil { 109 | return 110 | } 111 | for _, msg := range data { 112 | lastId = msg.ID 113 | select { 114 | case <-ctx.Done(): 115 | err = ctx.Err() 116 | return 117 | case msgCh <- msg: 118 | } 119 | } 120 | } 121 | }() 122 | return msgCh, msgErr 123 | } 124 | 125 | // Reads from a consumer group in a stream, using sema as the semaphore. It receives struct{}{} as "tokens" and reads a message for every token. 126 | func (r *RedisService) ReadStreamGroup(ctx context.Context, key string, group string, consumer string, sema <-chan struct{}) (<-chan *XREADMsg, <-chan error) { 127 | msgCh := make(chan *XREADMsg) 128 | errCh := make(chan error, 1) 129 | go func() { 130 | var count int 131 | var err error 132 | lastId := "0" 133 | defer func() { 134 | errCh <- err 135 | close(msgCh) 136 | close(errCh) 137 | }() 138 | for { 139 | if count == 0 { 140 | select { 141 | case <-ctx.Done(): 142 | return 143 | case <-sema: 144 | count++ 145 | } 146 | } 147 | var data []*XREAD 148 | data, err = ParseXREAD(r.DoContext(ctx, "XREADGROUP", "GROUP", group, consumer, "COUNT", 1, "BLOCK", 60000, "STREAMS", key, lastId)) 149 | if err == redis.ErrNil { 150 | err = nil 151 | continue 152 | } 153 | if err != nil { 154 | return 155 | } 156 | if len(data) == 0 || len(data[0].Msgs) == 0 { 157 | lastId = ">" 158 | continue 159 | } 160 | for _, msg := range data[0].Msgs { 161 | if lastId != ">" { 162 | lastId = msg.ID 163 | } 164 | msgCh <- msg 165 | count-- 166 | } 167 | } 168 | }() 169 | return msgCh, errCh 170 | } 171 | -------------------------------------------------------------------------------- /svc/app/problem.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "encoding/json" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/gomodule/redigo/redis" 11 | "github.com/microcosm-cc/bluemonday" 12 | "github.com/syzoj/syzoj-ng-go/lib/rediskey" 13 | "github.com/syzoj/syzoj-ng-go/models" 14 | "github.com/volatiletech/null" 15 | "github.com/volatiletech/sqlboiler/queries/qm" 16 | "github.com/russross/blackfriday" 17 | ) 18 | 19 | type GetProblemsResponse struct { 20 | Problems []*GetProblemsResponseProblem 21 | } 22 | 23 | type GetProblemsResponseProblem struct { 24 | Id string `json:"id"` 25 | Title null.String `json:"title"` 26 | Tags []string `json:"tags"` 27 | SubmitNum int64 `json:"submit_num"` 28 | AcceptNum int64 `json:"accept_num"` 29 | LastSubmit null.String `json:"last_submit"` 30 | LastAccept null.String `json:"last_accept"` 31 | } 32 | 33 | func (a *App) getApiProblems(c *gin.Context) { 34 | ctx := c.Request.Context() 35 | resp := &GetProblemsResponse{Problems: []*GetProblemsResponseProblem{}} 36 | problems, err := models.Problems(qm.Where("is_public=1"), qm.OrderBy("id ASC")).All(ctx, a.Db) 37 | if err != nil { 38 | c.AbortWithError(500, err) 39 | return 40 | } 41 | pipeline, err := a.Redis.NewPipeline(ctx) 42 | if err != nil { 43 | c.AbortWithError(500, err) 44 | return 45 | } 46 | defer pipeline.Close() 47 | userIdInt := c.GetInt(GIN_USER_ID) 48 | var userId string 49 | if userIdInt != 0 { 50 | userId = strconv.Itoa(userIdInt) 51 | } 52 | for _, problem := range problems { 53 | prob := &GetProblemsResponseProblem{Tags: []string{}} 54 | prob.Title = problem.Title 55 | if problem.Tags.Valid { 56 | json.Unmarshal([]byte(problem.Tags.String), &prob.Tags) 57 | } 58 | resp.Problems = append(resp.Problems, prob) 59 | probId := strconv.Itoa(problem.ID) 60 | prob.Id = probId 61 | pipeline.Do(func(data interface{}, err error) { 62 | num, err := redis.Int64(data, err) 63 | if err != nil { 64 | log.WithField("problem_id", probId).WithError(err).Error("failed to get problem submit count") 65 | return 66 | } 67 | prob.SubmitNum = num 68 | }, "GET", rediskey.MAIN_PROBLEM_SUBMITS.Format(probId)) 69 | pipeline.Do(func(data interface{}, err error) { 70 | num, err := redis.Int64(data, err) 71 | if err != nil { 72 | log.WithField("problem_id", probId).WithError(err).Error("failed to get problem submit count") 73 | return 74 | } 75 | prob.AcceptNum = num 76 | }, "GET", rediskey.MAIN_PROBLEM_ACCEPTS.Format(probId)) 77 | if userId != "" { 78 | pipeline.Do(func(data interface{}, err error) { 79 | sid, err := redis.String(data, err) 80 | if err == redis.ErrNil { 81 | return 82 | } 83 | if err != nil { 84 | log.WithField("user_id", userId).WithError(err).Error("failed to get user last submission") 85 | return 86 | } 87 | prob.LastSubmit = null.StringFrom(sid) 88 | }, "HGET", rediskey.MAIN_USER_LAST_SUBMISSION.Format(userId), probId) 89 | pipeline.Do(func(data interface{}, err error) { 90 | sid, err := redis.String(data, err) 91 | if err == redis.ErrNil { 92 | return 93 | } 94 | if err != nil { 95 | log.WithField("user_id", userId).WithError(err).Error("failed to get user last submission") 96 | return 97 | } 98 | prob.LastAccept = null.StringFrom(sid) 99 | }, "HGET", rediskey.MAIN_USER_LAST_ACCEPT.Format(userId), probId) 100 | } 101 | } 102 | if err := pipeline.Flush(ctx); err != nil { 103 | c.AbortWithError(500, err) 104 | return 105 | } 106 | c.JSON(200, resp) 107 | } 108 | 109 | type GetProblemResponse struct { 110 | Title string `json:"title"` 111 | Body string `json:"body"` 112 | } 113 | 114 | func (a *App) getApiProblem(c *gin.Context) { 115 | ctx := c.Request.Context() 116 | problemId, err := strconv.Atoi(c.Param("problem_id")) 117 | if err != nil { 118 | c.AbortWithError(400, err) 119 | return 120 | } 121 | problem, err := models.Problems(qm.Where("id=?", problemId)).One(ctx, a.Db) 122 | if err == sql.ErrNoRows { 123 | c.AbortWithError(404, err) 124 | return 125 | } 126 | if err != nil { 127 | c.AbortWithError(500, err) 128 | return 129 | } 130 | 131 | if problem.IsPublic.Int8 != 1 { 132 | c.JSON(200, gin.H{"error": "您没有权限进行此操作。"}) 133 | return 134 | } 135 | 136 | body := &bytes.Buffer{} 137 | resp := &GetProblemResponse{} 138 | resp.Title = problem.Title.String 139 | if problem.Description.String != "" { 140 | body.WriteString("# 题目描述\n\n" + problem.Description.String) 141 | } 142 | if problem.InputFormat.String != "" { 143 | body.WriteString("# 输入格式\n\n" + problem.InputFormat.String) 144 | } 145 | if problem.OutputFormat.String != "" { 146 | body.WriteString("# 输出格式\n\n" + problem.OutputFormat.String) 147 | } 148 | if problem.Example.String != "" { 149 | body.WriteString("# 样例\n\n" + problem.Example.String) 150 | } 151 | if problem.LimitAndHint.String != "" { 152 | body.WriteString("# 数据范围与提示\n\n" + problem.LimitAndHint.String) 153 | } 154 | resp.Body = string(bluemonday.UGCPolicy().SanitizeBytes(blackfriday.Run(body.Bytes()))) 155 | c.JSON(200, resp) 156 | } 157 | -------------------------------------------------------------------------------- /svc/judge/judge.go: -------------------------------------------------------------------------------- 1 | // Judge provides the judging infrastructure. 2 | package judge 3 | 4 | import ( 5 | "context" 6 | "database/sql" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | "github.com/sirupsen/logrus" 11 | "github.com/syzoj/syzoj-ng-go/lib/rediskey" 12 | svcredis "github.com/syzoj/syzoj-ng-go/svc/redis" 13 | "github.com/volatiletech/null" 14 | ) 15 | 16 | var log = logrus.StandardLogger() 17 | 18 | type JudgeService struct { 19 | Db *sql.DB 20 | Redis *svcredis.RedisService 21 | } 22 | 23 | func DefaultJudgeService(db *sql.DB, r *svcredis.RedisService) *JudgeService { 24 | return &JudgeService{Db: db, Redis: r} 25 | } 26 | 27 | func (s *JudgeService) Run(ctx context.Context) error { 28 | _, err := s.Redis.DoContext(ctx, "XGROUP", "CREATE", rediskey.CORE_QUEUE.Format("default"), "judger", "$", "MKSTREAM") 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | 35 | // ConvertResult 36 | type Judge struct { 37 | TaskId null.String `json:"task_id,omitempty"` 38 | Type null.Int `json:"type,omitempty"` 39 | Progress *JudgeProgress `json:"progress,omitempty"` 40 | } 41 | type JudgeProgress struct { 42 | Compile *JudgeProgressCompile `json:"compile,omitempty"` 43 | Judge *JudgeProgressJudge `json:"judge,omitempty"` 44 | Error null.Int `json:"error,omitempty"` 45 | SystemMessage null.String `json:"systemMessage,omitempty"` 46 | Status null.Int `json;"status,omitempty"` 47 | Message null.String `json:"message,omitempty"` 48 | } 49 | type JudgeProgressCompile struct { 50 | Status null.Int `json:"status"` 51 | } 52 | type JudgeProgressJudge struct { 53 | Subtasks []*JudgeProgressJudgeSubtask `json:"subtasks,omitempty"` 54 | } 55 | type JudgeProgressJudgeSubtask struct { 56 | Type null.Int `json:"type,omitempty"` 57 | Cases []*JudgeProgressJudgeSubtaskCase `json:"cases,omitempty"` 58 | Score null.Float64 `json:"score,omitempty"` 59 | } 60 | type JudgeProgressJudgeSubtaskCase struct { 61 | Status null.Int `json:"status,omitempty"` 62 | Result *JudgeProgressCaseResult `json:"result,omitempty"` 63 | ErrorMessage null.String `json:"errorMessage,omitempty"` 64 | } 65 | type JudgeProgressCaseResult struct { 66 | Type null.Int `json:"type,omitempty"` 67 | Time null.Float64 `json:"time,omitempty"` 68 | Memory null.Float64 `json:"memory,omitempty"` 69 | Status null.Int `json:"status,omitempty"` 70 | Input *FileContent `json:"input,omitempty"` 71 | Output *FileContent `json:"output,omitempty"` 72 | Score null.Float64 `json:"score,omitempty"` 73 | ScoringRate null.Float64 `json:"scoringRate,omitempty"` // ??? 74 | UserOutput null.String `json:"userOutput,omitempty'` 75 | UserError null.String `json:"userError,omitempty"` 76 | SpjMessage null.String `json:"spjMessage,omitempty"` 77 | SystemMessage null.String `json:"systemMessage,omitempty"` 78 | } 79 | type FileContent struct { 80 | Content string `json:"content"` 81 | Name string `json:"name"` 82 | } 83 | type SubmissionResult struct { 84 | Score null.Int `json:"score,omitempty"` 85 | Pending null.Bool `json:"pending,omitempty"` 86 | StatusString null.String `json:"statusString,omitempty"` 87 | Time null.Float64 `json:"time,omitempty"` 88 | Memory null.Float64 `json:"memory,omitempty"` 89 | Result null.String `json:"result,omitempty"` 90 | } 91 | 92 | var statusString = map[int]string{ 93 | 1: "Accepted", 94 | 2: "Wrong Answer", 95 | 3: "Partially Correct", 96 | 4: "Memory Limit Exceeded", 97 | 5: "Time Limit Exceeded", 98 | 6: "Output Limit Exceeded", 99 | 7: "Runtime Error", 100 | 8: "File Error", 101 | 9: "Judgement Failed", 102 | 10: "Invalid Interaction", 103 | } 104 | 105 | // TODO: updateRelatedInfo 106 | type SubmissionResultSummary struct { 107 | Status string 108 | Time null.Float64 109 | Memory null.Float64 110 | Score null.Float64 111 | } 112 | 113 | func GetSummary(res *Judge) *SubmissionResultSummary { 114 | var ( 115 | status string 116 | rtime null.Float64 117 | memory null.Float64 118 | score null.Float64 119 | ) 120 | prog := res.Progress 121 | if prog.Compile != nil && prog.Compile.Status == null.IntFrom(3) { 122 | status = "Compile Error" 123 | } else if prog.Error.Valid { 124 | switch prog.Error.Int { 125 | case 0: 126 | status = "No Testdata" 127 | default: 128 | status = "System Error" 129 | } 130 | } else if prog.Judge != nil && prog.Judge.Subtasks != nil { 131 | rtime = null.Float64From(0) 132 | memory = null.Float64From(0) 133 | for _, subtask := range prog.Judge.Subtasks { 134 | if subtask == nil { 135 | continue 136 | } 137 | for _, c := range subtask.Cases { 138 | if c.Result != nil { 139 | rtime.Float64 += c.Result.Time.Float64 140 | if c.Result.Memory.Valid && memory.Float64 < c.Result.Memory.Float64 { 141 | memory.Float64 = c.Result.Memory.Float64 142 | } 143 | if status == "" && c.Result.Type != null.IntFrom(1) { 144 | status = statusString[c.Result.Type.Int] 145 | } 146 | } 147 | } 148 | score.Float64 += subtask.Score.Float64 149 | } 150 | if status == "" { 151 | status = statusString[1] 152 | } 153 | } else { 154 | status = "System Error" 155 | } 156 | return &SubmissionResultSummary{ 157 | Status: status, 158 | Time: rtime, 159 | Memory: memory, 160 | Score: score, 161 | } 162 | } 163 | 164 | func (s *JudgeService) SaveTask(ctx context.Context, sid string, res *Judge) error { 165 | 166 | // Notify callbacks 167 | keys, err := redis.Values(s.Redis.DoContext(ctx, "SMEMBERS", rediskey.CORE_SUBMISSION_CALLBACK.Format(sid))) 168 | if err != nil { 169 | return err 170 | } 171 | pipe, err := s.Redis.NewPipeline(ctx) 172 | if err != nil { 173 | return err 174 | } 175 | defer pipe.Close() 176 | for _, key := range keys { 177 | s, err := redis.String(key, nil) 178 | if err != nil { 179 | return err 180 | } 181 | pipe.Do(nil, "XADD", s, "*", "sid", sid) 182 | } 183 | // Make submission expire 184 | pipe.Do(nil, "EXPIRE", rediskey.CORE_SUBMISSION_PROGRESS.Format(sid), int64(rediskey.DEFAULT_EXPIRE/time.Second)) 185 | pipe.Do(nil, "EXPIRE", rediskey.CORE_SUBMISSION_DATA.Format(sid), int64(rediskey.DEFAULT_EXPIRE/time.Second)) 186 | pipe.Do(nil, "EXPIRE", rediskey.CORE_SUBMISSION_RESULT.Format(sid), int64(rediskey.DEFAULT_EXPIRE/time.Second)) 187 | pipe.Do(nil, "EXPIRE", rediskey.CORE_SUBMISSION_CALLBACK.Format(sid), int64(rediskey.DEFAULT_EXPIRE/time.Second)) 188 | return pipe.Flush(ctx) 189 | } 190 | -------------------------------------------------------------------------------- /models/mysql_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "database/sql" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strings" 16 | 17 | _ "github.com/go-sql-driver/mysql" 18 | "github.com/kat-co/vala" 19 | "github.com/pkg/errors" 20 | "github.com/spf13/viper" 21 | "github.com/volatiletech/sqlboiler/drivers/sqlboiler-mysql/driver" 22 | "github.com/volatiletech/sqlboiler/randomize" 23 | ) 24 | 25 | var rgxMySQLkey = regexp.MustCompile(`(?m)((,\n)?\s+CONSTRAINT.*?FOREIGN KEY.*?\n)+`) 26 | 27 | type mysqlTester struct { 28 | dbConn *sql.DB 29 | 30 | dbName string 31 | host string 32 | user string 33 | pass string 34 | sslmode string 35 | port int 36 | 37 | optionFile string 38 | 39 | testDBName string 40 | skipSQLCmd bool 41 | } 42 | 43 | func init() { 44 | dbMain = &mysqlTester{} 45 | } 46 | 47 | func (m *mysqlTester) setup() error { 48 | var err error 49 | 50 | viper.SetDefault("mysql.sslmode", "true") 51 | viper.SetDefault("mysql.port", 3306) 52 | 53 | m.dbName = viper.GetString("mysql.dbname") 54 | m.host = viper.GetString("mysql.host") 55 | m.user = viper.GetString("mysql.user") 56 | m.pass = viper.GetString("mysql.pass") 57 | m.port = viper.GetInt("mysql.port") 58 | m.sslmode = viper.GetString("mysql.sslmode") 59 | m.testDBName = viper.GetString("mysql.testdbname") 60 | m.skipSQLCmd = viper.GetBool("mysql.skipsqlcmd") 61 | 62 | err = vala.BeginValidation().Validate( 63 | vala.StringNotEmpty(m.user, "mysql.user"), 64 | vala.StringNotEmpty(m.host, "mysql.host"), 65 | vala.Not(vala.Equals(m.port, 0, "mysql.port")), 66 | vala.StringNotEmpty(m.dbName, "mysql.dbname"), 67 | vala.StringNotEmpty(m.sslmode, "mysql.sslmode"), 68 | ).Check() 69 | 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Create a randomized db name. 75 | if len(m.testDBName) == 0 { 76 | m.testDBName = randomize.StableDBName(m.dbName) 77 | } 78 | 79 | if err = m.makeOptionFile(); err != nil { 80 | return errors.Wrap(err, "couldn't make option file") 81 | } 82 | 83 | if !m.skipSQLCmd { 84 | if err = m.dropTestDB(); err != nil { 85 | return err 86 | } 87 | if err = m.createTestDB(); err != nil { 88 | return err 89 | } 90 | 91 | dumpCmd := exec.Command("mysqldump", m.defaultsFile(), "--no-data", m.dbName) 92 | createCmd := exec.Command("mysql", m.defaultsFile(), "--database", m.testDBName) 93 | 94 | r, w := io.Pipe() 95 | dumpCmdStderr := &bytes.Buffer{} 96 | createCmdStderr := &bytes.Buffer{} 97 | 98 | dumpCmd.Stdout = w 99 | dumpCmd.Stderr = dumpCmdStderr 100 | 101 | createCmd.Stdin = newFKeyDestroyer(rgxMySQLkey, r) 102 | createCmd.Stderr = createCmdStderr 103 | 104 | if err = dumpCmd.Start(); err != nil { 105 | return errors.Wrap(err, "failed to start mysqldump command") 106 | } 107 | if err = createCmd.Start(); err != nil { 108 | return errors.Wrap(err, "failed to start mysql command") 109 | } 110 | 111 | if err = dumpCmd.Wait(); err != nil { 112 | fmt.Println(err) 113 | fmt.Println(dumpCmdStderr.String()) 114 | return errors.Wrap(err, "failed to wait for mysqldump command") 115 | } 116 | 117 | _ = w.Close() // After dumpCmd is done, close the write end of the pipe 118 | 119 | if err = createCmd.Wait(); err != nil { 120 | fmt.Println(err) 121 | fmt.Println(createCmdStderr.String()) 122 | return errors.Wrap(err, "failed to wait for mysql command") 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func (m *mysqlTester) sslMode(mode string) string { 130 | switch mode { 131 | case "true": 132 | return "REQUIRED" 133 | case "false": 134 | return "DISABLED" 135 | default: 136 | return "PREFERRED" 137 | } 138 | } 139 | 140 | func (m *mysqlTester) defaultsFile() string { 141 | return fmt.Sprintf("--defaults-file=%s", m.optionFile) 142 | } 143 | 144 | func (m *mysqlTester) makeOptionFile() error { 145 | tmp, err := ioutil.TempFile("", "optionfile") 146 | if err != nil { 147 | return errors.Wrap(err, "failed to create option file") 148 | } 149 | 150 | isTCP := false 151 | _, err = os.Stat(m.host) 152 | if os.IsNotExist(err) { 153 | isTCP = true 154 | } else if err != nil { 155 | return errors.Wrap(err, "could not stat m.host") 156 | } 157 | 158 | fmt.Fprintln(tmp, "[client]") 159 | fmt.Fprintf(tmp, "host=%s\n", m.host) 160 | fmt.Fprintf(tmp, "port=%d\n", m.port) 161 | fmt.Fprintf(tmp, "user=%s\n", m.user) 162 | if len(m.pass) != 0 { 163 | fmt.Fprintf(tmp, "password=%s\n", m.pass) 164 | } 165 | fmt.Fprintf(tmp, "ssl-mode=%s\n", m.sslMode(m.sslmode)) 166 | if isTCP { 167 | fmt.Fprintln(tmp, "protocol=tcp") 168 | } 169 | 170 | fmt.Fprintln(tmp, "[mysqldump]") 171 | fmt.Fprintf(tmp, "host=%s\n", m.host) 172 | fmt.Fprintf(tmp, "port=%d\n", m.port) 173 | fmt.Fprintf(tmp, "user=%s\n", m.user) 174 | if len(m.pass) != 0 { 175 | fmt.Fprintf(tmp, "password=%s\n", m.pass) 176 | } 177 | fmt.Fprintf(tmp, "ssl-mode=%s\n", m.sslMode(m.sslmode)) 178 | if isTCP { 179 | fmt.Fprintln(tmp, "protocol=tcp") 180 | } 181 | 182 | m.optionFile = tmp.Name() 183 | 184 | return tmp.Close() 185 | } 186 | 187 | func (m *mysqlTester) createTestDB() error { 188 | sql := fmt.Sprintf("create database %s;", m.testDBName) 189 | return m.runCmd(sql, "mysql") 190 | } 191 | 192 | func (m *mysqlTester) dropTestDB() error { 193 | sql := fmt.Sprintf("drop database if exists %s;", m.testDBName) 194 | return m.runCmd(sql, "mysql") 195 | } 196 | 197 | func (m *mysqlTester) teardown() error { 198 | if m.dbConn != nil { 199 | return m.dbConn.Close() 200 | } 201 | 202 | if !m.skipSQLCmd { 203 | if err := m.dropTestDB(); err != nil { 204 | return err 205 | } 206 | } 207 | 208 | return os.Remove(m.optionFile) 209 | } 210 | 211 | func (m *mysqlTester) runCmd(stdin, command string, args ...string) error { 212 | args = append([]string{m.defaultsFile()}, args...) 213 | 214 | cmd := exec.Command(command, args...) 215 | cmd.Stdin = strings.NewReader(stdin) 216 | 217 | stdout := &bytes.Buffer{} 218 | stderr := &bytes.Buffer{} 219 | cmd.Stdout = stdout 220 | cmd.Stderr = stderr 221 | if err := cmd.Run(); err != nil { 222 | fmt.Println("failed running:", command, args) 223 | fmt.Println(stdout.String()) 224 | fmt.Println(stderr.String()) 225 | return err 226 | } 227 | 228 | return nil 229 | } 230 | 231 | func (m *mysqlTester) conn() (*sql.DB, error) { 232 | if m.dbConn != nil { 233 | return m.dbConn, nil 234 | } 235 | 236 | var err error 237 | m.dbConn, err = sql.Open("mysql", driver.MySQLBuildQueryString(m.user, m.pass, m.testDBName, m.host, m.port, m.sslmode)) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | return m.dbConn, nil 243 | } 244 | -------------------------------------------------------------------------------- /svc/app/judge.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "regexp" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gin-contrib/sse" 12 | "github.com/gin-gonic/gin" 13 | "github.com/gomodule/redigo/redis" 14 | "github.com/gorilla/websocket" 15 | "github.com/syzoj/syzoj-ng-go/lib/rediskey" 16 | "github.com/syzoj/syzoj-ng-go/models" 17 | "github.com/syzoj/syzoj-ng-go/svc/judge" 18 | svcredis "github.com/syzoj/syzoj-ng-go/svc/redis" 19 | "github.com/volatiletech/null" 20 | "github.com/volatiletech/sqlboiler/boil" 21 | "github.com/volatiletech/sqlboiler/queries/qm" 22 | ) 23 | 24 | // # Judge core 25 | // This part of code is designed to be stateless and all state is stored in Redis for persistence. 26 | // List of redis keys used: 27 | // 1. rediskey.CORE_QUEUE: A Redis 5 stream where each entry is a submission. Every entry has the following keys: 28 | // - sid: Submission id. 29 | // - data: additional related data in JSON format. 30 | // It has a consumer group called "judger". Judgers are identified by their username in HTTP basic auth. The username is used as the Redis consumer name. 31 | // Redis mechanisms should be used to handle failures. 32 | // 2. rediskey.CORE_SUBMISSION_PROGRESS: A Redis 5 stream containing submission progress. Every entry has the following keys: 33 | // - type: One of "reset", "progress", "done". 34 | // - data: JSON data if type is "progress". 35 | // ## Judger protocol 36 | // The judger endpoints use HTTP basic authentication. Username must match [0-9A-Za-z_]{3,16} and password is the judger token in App.JudgeToken. 37 | // The judger connects to /judger/wait-for-task and upgrades the connection to a WebSocket. Then it receives tasks, process the task, send progress and result, and receive another task and loop. 38 | // While processing a task, the judger sends messages with type="progress" to the broker. When the judger has done with the task, it sends a message with type="finish". 39 | // The broker saves the result to the database and emits a type="done" event. 40 | 41 | const GIN_JUDGER = "github.com/syzoj/syzoj-ng-go/svc/app.judger" 42 | 43 | func (a *App) ensureQueue(ctx context.Context, queueName string) error { 44 | _, err := a.Redis.DoContext(ctx, "XGROUP", "CREATE", rediskey.CORE_QUEUE.Format(queueName), "judger", "$", "MKSTREAM") 45 | if rerr, ok := err.(redis.Error); ok { 46 | if rerr[:10] == "BUSYGROUP " { 47 | return nil 48 | } 49 | } 50 | return err 51 | } 52 | 53 | var judgerRegexp = regexp.MustCompile("[0-9A-Za-z_]{3,16}") 54 | 55 | func (a *App) useCheckJudgeToken(c *gin.Context) { 56 | judger := c.Query("user") 57 | token := c.Query("token") 58 | if a.JudgeToken == "" || token != a.JudgeToken || !judgerRegexp.Match([]byte(judger)) { 59 | c.JSON(403, gin.H{"error": "Token doesn't match or invalid judger name (judger name must be [0-9A-Za-z_]{3,16})"}) 60 | return 61 | } 62 | c.Set(GIN_JUDGER, judger) 63 | } 64 | 65 | var wsUpgrader = websocket.Upgrader{} // TODO: configure ReadBufferSize and WriteBufferSize (seems to default to 4096) 66 | 67 | type TaskResult struct { 68 | Id string `json:"id"` 69 | Sid string `json:"sid"` 70 | Type string `json:"type"` 71 | Data json.RawMessage `json:"data"` 72 | } 73 | 74 | // TODO: move logic to judge 75 | func (a *App) getJudgeWaitForTask(c *gin.Context) { 76 | ctx, cancel := context.WithCancel(c.Request.Context()) 77 | defer cancel() 78 | 79 | conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) 80 | if err != nil { 81 | log.WithError(err).Error("failed to upgrade websocket") 82 | return 83 | } 84 | defer conn.Close() 85 | pipeline, err := a.Redis.NewPipeline(ctx) 86 | if err != nil { 87 | log.WithError(err).Error("failed to create redis pipeline") 88 | return 89 | } 90 | defer pipeline.Close() 91 | 92 | key := rediskey.CORE_QUEUE.Format("default") 93 | judger := c.GetString(GIN_JUDGER) 94 | sema := make(chan struct{}, 1) // queue capacity is 1 95 | sema <- struct{}{} 96 | var wg sync.WaitGroup 97 | wg.Add(2) 98 | // Fetch tasks 99 | go func() { 100 | defer wg.Done() 101 | msgCh, errCh := a.Redis.ReadStreamGroup(ctx, key, "judger", judger, sema) 102 | for { 103 | select { 104 | case <-ctx.Done(): 105 | return 106 | case msg := <-msgCh: 107 | sid := msg.Data["sid"] 108 | data, err := redis.Bytes(a.Redis.DoContext(ctx, "GET", rediskey.CORE_SUBMISSION_DATA.Format(sid))) 109 | if err != nil { 110 | log.WithError(err).Error("failed to call GET") 111 | return 112 | } 113 | pipeline.Do(a.checkRedis, "XADD", rediskey.CORE_SUBMISSION_PROGRESS.Format(sid), "*", "type", "reset") 114 | if err := pipeline.Flush(ctx); err != nil { 115 | log.WithError(err).Error("failed to call XADD") 116 | return 117 | } 118 | if err := conn.WriteJSON(gin.H{ 119 | "id": msg.ID, 120 | "sid": sid, 121 | "data": json.RawMessage(data), 122 | }); err != nil { 123 | log.WithError(err).Error("failed to send data") 124 | return 125 | } 126 | case err := <-errCh: 127 | if err != nil { 128 | log.WithError(err).Error("failed to read from Redis") 129 | } 130 | return 131 | } 132 | } 133 | }() 134 | 135 | // Receive results 136 | go func() { 137 | defer cancel() 138 | defer wg.Done() 139 | for { 140 | data := &TaskResult{} 141 | if err := conn.ReadJSON(data); err != nil { 142 | log.WithError(err).Error("failed to read data") 143 | return 144 | } 145 | skey := rediskey.CORE_SUBMISSION_PROGRESS.Format(data.Sid) 146 | switch data.Type { 147 | case "progress": 148 | pipeline.Do(a.checkRedis, "XADD", skey, "*", "type", "progress", "data", []byte(data.Data)) 149 | case "finish": 150 | res := &judge.Judge{} 151 | if err := json.Unmarshal(data.Data, res); err != nil { 152 | log.WithField("sid", data.Sid).WithError(err).Error("failed to parse data") 153 | return 154 | } 155 | if res.Type == null.IntFrom(4) { 156 | // Save submission result 157 | sum := judge.GetSummary(res) 158 | subm, err := models.JudgeStates(qm.Where("task_id=?", data.Sid)).One(ctx, a.Db) 159 | if err != nil { 160 | log.WithError(err).Error("failed to query db") 161 | return 162 | } 163 | subm.Score = null.IntFrom(int(sum.Score.Float64)) 164 | subm.Pending = null.Int8From(0) 165 | subm.Status = null.StringFrom(sum.Status) 166 | subm.TotalTime = null.IntFrom(int(sum.Time.Float64)) 167 | subm.MaxMemory = null.IntFrom(int(sum.Memory.Float64)) 168 | progBytes, err := json.Marshal(res.Progress) 169 | if err != nil { 170 | panic(err) 171 | } 172 | subm.Result = null.StringFrom(string(progBytes)) 173 | if _, err := subm.Update(ctx, a.Db, boil.Blacklist()); err != nil { 174 | log.WithError(err).Error("failed to update db") 175 | return 176 | } 177 | pipeline.Do(a.checkRedis, "SET", rediskey.CORE_SUBMISSION_RESULT.Format(data.Sid), data.Data) 178 | if err := pipeline.Flush(ctx); err != nil { 179 | log.WithError(err).Error("failed to send redis") 180 | return 181 | } 182 | if err := a.JudgeService.SaveTask(ctx, data.Sid, res); err != nil { 183 | log.WithField("sid", data.Sid).WithError(err).Error("failed to save submission") 184 | return 185 | } 186 | pipeline.Do(a.checkRedis, "XADD", skey, "*", "type", "done") 187 | var res svcredis.RedisResult 188 | pipeline.Do(res.Save, "XACK", key, "judger", data.Id) 189 | if err := pipeline.Flush(ctx); err != nil { 190 | log.WithError(err).Error("failed to flush to Redis") 191 | return 192 | } 193 | n, err := redis.Int64(res.Result, res.Err) 194 | if err != nil { 195 | log.WithError(err).Error("failed to call XACK") 196 | return 197 | } 198 | for ; n > 0; n-- { 199 | select { 200 | case <-ctx.Done(): 201 | return 202 | case sema <- struct{}{}: 203 | } 204 | } 205 | } 206 | } 207 | } 208 | }() 209 | wg.Wait() 210 | } 211 | 212 | func (a *App) getTaskProgress(c *gin.Context) { 213 | ctx, cancel := context.WithCancel(c.Request.Context()) 214 | defer cancel() 215 | key := rediskey.CORE_SUBMISSION_PROGRESS.Format(c.Param("sid")) 216 | lastId := c.Request.Header.Get("Last-Event-Id") 217 | if lastId == "" { 218 | lastId = "0" 219 | } 220 | c.Header("X-Accel-Buffering", "no") // need this to make SSE work through nginx 221 | c.Render(200, sse.Event{Event: "start"}) 222 | ping := time.NewTicker(time.Second * 30) 223 | defer ping.Stop() 224 | chMsg, chErr := a.Redis.ReadStream(ctx, key, lastId) 225 | for { 226 | select { 227 | case <-ctx.Done(): 228 | return 229 | case err := <-chErr: 230 | c.AbortWithError(500, err) 231 | return 232 | case msg := <-chMsg: 233 | var ev sse.Event 234 | ev.Id = msg.ID 235 | ev.Event = msg.Data["type"] 236 | if d, ok := msg.Data["data"]; ok { 237 | ev.Data = json.RawMessage(d) 238 | } 239 | c.Render(-1, ev) 240 | c.Writer.Flush() 241 | case <-ping.C: 242 | c.Render(-1, sse.Event{Event: "ping"}) 243 | c.Writer.Flush() 244 | } 245 | } 246 | } 247 | 248 | // Handle judge done events and update stats. 249 | func (a *App) handleJudgeDone(ctx context.Context) error { 250 | pipe, err := a.Redis.NewPipeline(ctx) 251 | if err != nil { 252 | return err 253 | } 254 | pipe.Do(nil, "XGROUP", "CREATE", rediskey.MAIN_JUDGE_DONE, "main", "$", "MKSTREAM") 255 | if err := pipe.Flush(ctx); err != nil { 256 | return err 257 | } 258 | sema := make(chan struct{}, 1) 259 | sema <- struct{}{} 260 | chMsg, chErr := a.Redis.ReadStreamGroup(ctx, rediskey.MAIN_JUDGE_DONE, "main", "main", sema) 261 | go func() { 262 | for { 263 | select { 264 | case <-ctx.Done(): 265 | return 266 | case <-time.After(time.Second): 267 | pipe.Do(nil, "PING") 268 | if err := pipe.Flush(ctx); err != nil { 269 | log.WithError(err).Error("failed to flush to redis") 270 | return 271 | } 272 | } 273 | } 274 | }() 275 | for { 276 | select { 277 | case <-ctx.Done(): 278 | return ctx.Err() 279 | case err := <-chErr: 280 | return err 281 | case msg := <-chMsg: 282 | sid := msg.Data["sid"] 283 | if sid == "" { 284 | log.WithField("id", msg.ID).Error("judge done stream: missing sid field") 285 | continue 286 | } 287 | subm, err := models.JudgeStates(qm.Where("task_id=?", sid)).One(ctx, a.Db) 288 | if err != nil { 289 | log.WithField("sid", sid).Error("judge done stream: failed to get submission") 290 | continue 291 | } 292 | if subm.ProblemID.Valid { 293 | pid := strconv.Itoa(subm.ProblemID.Int) 294 | pipe.Do(a.checkRedis, "INCR", rediskey.MAIN_PROBLEM_SUBMITS.Format(pid)) 295 | if subm.Status == null.StringFrom("Accepted") { 296 | pipe.Do(a.checkRedis, "INCR", rediskey.MAIN_PROBLEM_ACCEPTS.Format(pid)) 297 | } 298 | if subm.UserID.Valid { 299 | uid := strconv.Itoa(subm.UserID.Int) 300 | pipe.Do(a.checkRedis, "HSET", rediskey.MAIN_USER_LAST_SUBMISSION.Format(uid), pid, sid) 301 | if subm.Status == null.StringFrom("Accepted") { 302 | pipe.Do(a.checkRedis, "HSET", rediskey.MAIN_USER_LAST_ACCEPT.Format(uid), pid, sid) 303 | } 304 | } 305 | } 306 | pipe.Do(a.checkRedis, "XACK", rediskey.MAIN_JUDGE_DONE, "main", msg.ID) 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /models/boil_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | // This test suite runs each operation test in parallel. 9 | // Example, if your database has 3 tables, the suite will run: 10 | // table1, table2 and table3 Delete in parallel 11 | // table1, table2 and table3 Insert in parallel, and so forth. 12 | // It does NOT run each operation group in parallel. 13 | // Separating the tests thusly grants avoidance of Postgres deadlocks. 14 | func TestParent(t *testing.T) { 15 | t.Run("Articles", testArticles) 16 | t.Run("ArticleComments", testArticleComments) 17 | t.Run("Contests", testContests) 18 | t.Run("ContestPlayers", testContestPlayers) 19 | t.Run("ContestRanklists", testContestRanklists) 20 | t.Run("Files", testFiles) 21 | t.Run("FormattedCodes", testFormattedCodes) 22 | t.Run("JudgeStates", testJudgeStates) 23 | t.Run("Problems", testProblems) 24 | t.Run("ProblemTags", testProblemTags) 25 | t.Run("ProblemTagMaps", testProblemTagMaps) 26 | t.Run("RatingCalculations", testRatingCalculations) 27 | t.Run("RatingHistories", testRatingHistories) 28 | t.Run("SubmissionStatistics", testSubmissionStatistics) 29 | t.Run("Users", testUsers) 30 | t.Run("UserPrivileges", testUserPrivileges) 31 | } 32 | 33 | func TestDelete(t *testing.T) { 34 | t.Run("Articles", testArticlesDelete) 35 | t.Run("ArticleComments", testArticleCommentsDelete) 36 | t.Run("Contests", testContestsDelete) 37 | t.Run("ContestPlayers", testContestPlayersDelete) 38 | t.Run("ContestRanklists", testContestRanklistsDelete) 39 | t.Run("Files", testFilesDelete) 40 | t.Run("FormattedCodes", testFormattedCodesDelete) 41 | t.Run("JudgeStates", testJudgeStatesDelete) 42 | t.Run("Problems", testProblemsDelete) 43 | t.Run("ProblemTags", testProblemTagsDelete) 44 | t.Run("ProblemTagMaps", testProblemTagMapsDelete) 45 | t.Run("RatingCalculations", testRatingCalculationsDelete) 46 | t.Run("RatingHistories", testRatingHistoriesDelete) 47 | t.Run("SubmissionStatistics", testSubmissionStatisticsDelete) 48 | t.Run("Users", testUsersDelete) 49 | t.Run("UserPrivileges", testUserPrivilegesDelete) 50 | } 51 | 52 | func TestQueryDeleteAll(t *testing.T) { 53 | t.Run("Articles", testArticlesQueryDeleteAll) 54 | t.Run("ArticleComments", testArticleCommentsQueryDeleteAll) 55 | t.Run("Contests", testContestsQueryDeleteAll) 56 | t.Run("ContestPlayers", testContestPlayersQueryDeleteAll) 57 | t.Run("ContestRanklists", testContestRanklistsQueryDeleteAll) 58 | t.Run("Files", testFilesQueryDeleteAll) 59 | t.Run("FormattedCodes", testFormattedCodesQueryDeleteAll) 60 | t.Run("JudgeStates", testJudgeStatesQueryDeleteAll) 61 | t.Run("Problems", testProblemsQueryDeleteAll) 62 | t.Run("ProblemTags", testProblemTagsQueryDeleteAll) 63 | t.Run("ProblemTagMaps", testProblemTagMapsQueryDeleteAll) 64 | t.Run("RatingCalculations", testRatingCalculationsQueryDeleteAll) 65 | t.Run("RatingHistories", testRatingHistoriesQueryDeleteAll) 66 | t.Run("SubmissionStatistics", testSubmissionStatisticsQueryDeleteAll) 67 | t.Run("Users", testUsersQueryDeleteAll) 68 | t.Run("UserPrivileges", testUserPrivilegesQueryDeleteAll) 69 | } 70 | 71 | func TestSliceDeleteAll(t *testing.T) { 72 | t.Run("Articles", testArticlesSliceDeleteAll) 73 | t.Run("ArticleComments", testArticleCommentsSliceDeleteAll) 74 | t.Run("Contests", testContestsSliceDeleteAll) 75 | t.Run("ContestPlayers", testContestPlayersSliceDeleteAll) 76 | t.Run("ContestRanklists", testContestRanklistsSliceDeleteAll) 77 | t.Run("Files", testFilesSliceDeleteAll) 78 | t.Run("FormattedCodes", testFormattedCodesSliceDeleteAll) 79 | t.Run("JudgeStates", testJudgeStatesSliceDeleteAll) 80 | t.Run("Problems", testProblemsSliceDeleteAll) 81 | t.Run("ProblemTags", testProblemTagsSliceDeleteAll) 82 | t.Run("ProblemTagMaps", testProblemTagMapsSliceDeleteAll) 83 | t.Run("RatingCalculations", testRatingCalculationsSliceDeleteAll) 84 | t.Run("RatingHistories", testRatingHistoriesSliceDeleteAll) 85 | t.Run("SubmissionStatistics", testSubmissionStatisticsSliceDeleteAll) 86 | t.Run("Users", testUsersSliceDeleteAll) 87 | t.Run("UserPrivileges", testUserPrivilegesSliceDeleteAll) 88 | } 89 | 90 | func TestExists(t *testing.T) { 91 | t.Run("Articles", testArticlesExists) 92 | t.Run("ArticleComments", testArticleCommentsExists) 93 | t.Run("Contests", testContestsExists) 94 | t.Run("ContestPlayers", testContestPlayersExists) 95 | t.Run("ContestRanklists", testContestRanklistsExists) 96 | t.Run("Files", testFilesExists) 97 | t.Run("FormattedCodes", testFormattedCodesExists) 98 | t.Run("JudgeStates", testJudgeStatesExists) 99 | t.Run("Problems", testProblemsExists) 100 | t.Run("ProblemTags", testProblemTagsExists) 101 | t.Run("ProblemTagMaps", testProblemTagMapsExists) 102 | t.Run("RatingCalculations", testRatingCalculationsExists) 103 | t.Run("RatingHistories", testRatingHistoriesExists) 104 | t.Run("SubmissionStatistics", testSubmissionStatisticsExists) 105 | t.Run("Users", testUsersExists) 106 | t.Run("UserPrivileges", testUserPrivilegesExists) 107 | } 108 | 109 | func TestFind(t *testing.T) { 110 | t.Run("Articles", testArticlesFind) 111 | t.Run("ArticleComments", testArticleCommentsFind) 112 | t.Run("Contests", testContestsFind) 113 | t.Run("ContestPlayers", testContestPlayersFind) 114 | t.Run("ContestRanklists", testContestRanklistsFind) 115 | t.Run("Files", testFilesFind) 116 | t.Run("FormattedCodes", testFormattedCodesFind) 117 | t.Run("JudgeStates", testJudgeStatesFind) 118 | t.Run("Problems", testProblemsFind) 119 | t.Run("ProblemTags", testProblemTagsFind) 120 | t.Run("ProblemTagMaps", testProblemTagMapsFind) 121 | t.Run("RatingCalculations", testRatingCalculationsFind) 122 | t.Run("RatingHistories", testRatingHistoriesFind) 123 | t.Run("SubmissionStatistics", testSubmissionStatisticsFind) 124 | t.Run("Users", testUsersFind) 125 | t.Run("UserPrivileges", testUserPrivilegesFind) 126 | } 127 | 128 | func TestBind(t *testing.T) { 129 | t.Run("Articles", testArticlesBind) 130 | t.Run("ArticleComments", testArticleCommentsBind) 131 | t.Run("Contests", testContestsBind) 132 | t.Run("ContestPlayers", testContestPlayersBind) 133 | t.Run("ContestRanklists", testContestRanklistsBind) 134 | t.Run("Files", testFilesBind) 135 | t.Run("FormattedCodes", testFormattedCodesBind) 136 | t.Run("JudgeStates", testJudgeStatesBind) 137 | t.Run("Problems", testProblemsBind) 138 | t.Run("ProblemTags", testProblemTagsBind) 139 | t.Run("ProblemTagMaps", testProblemTagMapsBind) 140 | t.Run("RatingCalculations", testRatingCalculationsBind) 141 | t.Run("RatingHistories", testRatingHistoriesBind) 142 | t.Run("SubmissionStatistics", testSubmissionStatisticsBind) 143 | t.Run("Users", testUsersBind) 144 | t.Run("UserPrivileges", testUserPrivilegesBind) 145 | } 146 | 147 | func TestOne(t *testing.T) { 148 | t.Run("Articles", testArticlesOne) 149 | t.Run("ArticleComments", testArticleCommentsOne) 150 | t.Run("Contests", testContestsOne) 151 | t.Run("ContestPlayers", testContestPlayersOne) 152 | t.Run("ContestRanklists", testContestRanklistsOne) 153 | t.Run("Files", testFilesOne) 154 | t.Run("FormattedCodes", testFormattedCodesOne) 155 | t.Run("JudgeStates", testJudgeStatesOne) 156 | t.Run("Problems", testProblemsOne) 157 | t.Run("ProblemTags", testProblemTagsOne) 158 | t.Run("ProblemTagMaps", testProblemTagMapsOne) 159 | t.Run("RatingCalculations", testRatingCalculationsOne) 160 | t.Run("RatingHistories", testRatingHistoriesOne) 161 | t.Run("SubmissionStatistics", testSubmissionStatisticsOne) 162 | t.Run("Users", testUsersOne) 163 | t.Run("UserPrivileges", testUserPrivilegesOne) 164 | } 165 | 166 | func TestAll(t *testing.T) { 167 | t.Run("Articles", testArticlesAll) 168 | t.Run("ArticleComments", testArticleCommentsAll) 169 | t.Run("Contests", testContestsAll) 170 | t.Run("ContestPlayers", testContestPlayersAll) 171 | t.Run("ContestRanklists", testContestRanklistsAll) 172 | t.Run("Files", testFilesAll) 173 | t.Run("FormattedCodes", testFormattedCodesAll) 174 | t.Run("JudgeStates", testJudgeStatesAll) 175 | t.Run("Problems", testProblemsAll) 176 | t.Run("ProblemTags", testProblemTagsAll) 177 | t.Run("ProblemTagMaps", testProblemTagMapsAll) 178 | t.Run("RatingCalculations", testRatingCalculationsAll) 179 | t.Run("RatingHistories", testRatingHistoriesAll) 180 | t.Run("SubmissionStatistics", testSubmissionStatisticsAll) 181 | t.Run("Users", testUsersAll) 182 | t.Run("UserPrivileges", testUserPrivilegesAll) 183 | } 184 | 185 | func TestCount(t *testing.T) { 186 | t.Run("Articles", testArticlesCount) 187 | t.Run("ArticleComments", testArticleCommentsCount) 188 | t.Run("Contests", testContestsCount) 189 | t.Run("ContestPlayers", testContestPlayersCount) 190 | t.Run("ContestRanklists", testContestRanklistsCount) 191 | t.Run("Files", testFilesCount) 192 | t.Run("FormattedCodes", testFormattedCodesCount) 193 | t.Run("JudgeStates", testJudgeStatesCount) 194 | t.Run("Problems", testProblemsCount) 195 | t.Run("ProblemTags", testProblemTagsCount) 196 | t.Run("ProblemTagMaps", testProblemTagMapsCount) 197 | t.Run("RatingCalculations", testRatingCalculationsCount) 198 | t.Run("RatingHistories", testRatingHistoriesCount) 199 | t.Run("SubmissionStatistics", testSubmissionStatisticsCount) 200 | t.Run("Users", testUsersCount) 201 | t.Run("UserPrivileges", testUserPrivilegesCount) 202 | } 203 | 204 | func TestHooks(t *testing.T) { 205 | t.Run("Articles", testArticlesHooks) 206 | t.Run("ArticleComments", testArticleCommentsHooks) 207 | t.Run("Contests", testContestsHooks) 208 | t.Run("ContestPlayers", testContestPlayersHooks) 209 | t.Run("ContestRanklists", testContestRanklistsHooks) 210 | t.Run("Files", testFilesHooks) 211 | t.Run("FormattedCodes", testFormattedCodesHooks) 212 | t.Run("JudgeStates", testJudgeStatesHooks) 213 | t.Run("Problems", testProblemsHooks) 214 | t.Run("ProblemTags", testProblemTagsHooks) 215 | t.Run("ProblemTagMaps", testProblemTagMapsHooks) 216 | t.Run("RatingCalculations", testRatingCalculationsHooks) 217 | t.Run("RatingHistories", testRatingHistoriesHooks) 218 | t.Run("SubmissionStatistics", testSubmissionStatisticsHooks) 219 | t.Run("Users", testUsersHooks) 220 | t.Run("UserPrivileges", testUserPrivilegesHooks) 221 | } 222 | 223 | func TestInsert(t *testing.T) { 224 | t.Run("Articles", testArticlesInsert) 225 | t.Run("Articles", testArticlesInsertWhitelist) 226 | t.Run("ArticleComments", testArticleCommentsInsert) 227 | t.Run("ArticleComments", testArticleCommentsInsertWhitelist) 228 | t.Run("Contests", testContestsInsert) 229 | t.Run("Contests", testContestsInsertWhitelist) 230 | t.Run("ContestPlayers", testContestPlayersInsert) 231 | t.Run("ContestPlayers", testContestPlayersInsertWhitelist) 232 | t.Run("ContestRanklists", testContestRanklistsInsert) 233 | t.Run("ContestRanklists", testContestRanklistsInsertWhitelist) 234 | t.Run("Files", testFilesInsert) 235 | t.Run("Files", testFilesInsertWhitelist) 236 | t.Run("FormattedCodes", testFormattedCodesInsert) 237 | t.Run("FormattedCodes", testFormattedCodesInsertWhitelist) 238 | t.Run("JudgeStates", testJudgeStatesInsert) 239 | t.Run("JudgeStates", testJudgeStatesInsertWhitelist) 240 | t.Run("Problems", testProblemsInsert) 241 | t.Run("Problems", testProblemsInsertWhitelist) 242 | t.Run("ProblemTags", testProblemTagsInsert) 243 | t.Run("ProblemTags", testProblemTagsInsertWhitelist) 244 | t.Run("ProblemTagMaps", testProblemTagMapsInsert) 245 | t.Run("ProblemTagMaps", testProblemTagMapsInsertWhitelist) 246 | t.Run("RatingCalculations", testRatingCalculationsInsert) 247 | t.Run("RatingCalculations", testRatingCalculationsInsertWhitelist) 248 | t.Run("RatingHistories", testRatingHistoriesInsert) 249 | t.Run("RatingHistories", testRatingHistoriesInsertWhitelist) 250 | t.Run("SubmissionStatistics", testSubmissionStatisticsInsert) 251 | t.Run("SubmissionStatistics", testSubmissionStatisticsInsertWhitelist) 252 | t.Run("Users", testUsersInsert) 253 | t.Run("Users", testUsersInsertWhitelist) 254 | t.Run("UserPrivileges", testUserPrivilegesInsert) 255 | t.Run("UserPrivileges", testUserPrivilegesInsertWhitelist) 256 | } 257 | 258 | // TestToOne tests cannot be run in parallel 259 | // or deadlocks can occur. 260 | func TestToOne(t *testing.T) {} 261 | 262 | // TestOneToOne tests cannot be run in parallel 263 | // or deadlocks can occur. 264 | func TestOneToOne(t *testing.T) {} 265 | 266 | // TestToMany tests cannot be run in parallel 267 | // or deadlocks can occur. 268 | func TestToMany(t *testing.T) {} 269 | 270 | // TestToOneSet tests cannot be run in parallel 271 | // or deadlocks can occur. 272 | func TestToOneSet(t *testing.T) {} 273 | 274 | // TestToOneRemove tests cannot be run in parallel 275 | // or deadlocks can occur. 276 | func TestToOneRemove(t *testing.T) {} 277 | 278 | // TestOneToOneSet tests cannot be run in parallel 279 | // or deadlocks can occur. 280 | func TestOneToOneSet(t *testing.T) {} 281 | 282 | // TestOneToOneRemove tests cannot be run in parallel 283 | // or deadlocks can occur. 284 | func TestOneToOneRemove(t *testing.T) {} 285 | 286 | // TestToManyAdd tests cannot be run in parallel 287 | // or deadlocks can occur. 288 | func TestToManyAdd(t *testing.T) {} 289 | 290 | // TestToManySet tests cannot be run in parallel 291 | // or deadlocks can occur. 292 | func TestToManySet(t *testing.T) {} 293 | 294 | // TestToManyRemove tests cannot be run in parallel 295 | // or deadlocks can occur. 296 | func TestToManyRemove(t *testing.T) {} 297 | 298 | func TestReload(t *testing.T) { 299 | t.Run("Articles", testArticlesReload) 300 | t.Run("ArticleComments", testArticleCommentsReload) 301 | t.Run("Contests", testContestsReload) 302 | t.Run("ContestPlayers", testContestPlayersReload) 303 | t.Run("ContestRanklists", testContestRanklistsReload) 304 | t.Run("Files", testFilesReload) 305 | t.Run("FormattedCodes", testFormattedCodesReload) 306 | t.Run("JudgeStates", testJudgeStatesReload) 307 | t.Run("Problems", testProblemsReload) 308 | t.Run("ProblemTags", testProblemTagsReload) 309 | t.Run("ProblemTagMaps", testProblemTagMapsReload) 310 | t.Run("RatingCalculations", testRatingCalculationsReload) 311 | t.Run("RatingHistories", testRatingHistoriesReload) 312 | t.Run("SubmissionStatistics", testSubmissionStatisticsReload) 313 | t.Run("Users", testUsersReload) 314 | t.Run("UserPrivileges", testUserPrivilegesReload) 315 | } 316 | 317 | func TestReloadAll(t *testing.T) { 318 | t.Run("Articles", testArticlesReloadAll) 319 | t.Run("ArticleComments", testArticleCommentsReloadAll) 320 | t.Run("Contests", testContestsReloadAll) 321 | t.Run("ContestPlayers", testContestPlayersReloadAll) 322 | t.Run("ContestRanklists", testContestRanklistsReloadAll) 323 | t.Run("Files", testFilesReloadAll) 324 | t.Run("FormattedCodes", testFormattedCodesReloadAll) 325 | t.Run("JudgeStates", testJudgeStatesReloadAll) 326 | t.Run("Problems", testProblemsReloadAll) 327 | t.Run("ProblemTags", testProblemTagsReloadAll) 328 | t.Run("ProblemTagMaps", testProblemTagMapsReloadAll) 329 | t.Run("RatingCalculations", testRatingCalculationsReloadAll) 330 | t.Run("RatingHistories", testRatingHistoriesReloadAll) 331 | t.Run("SubmissionStatistics", testSubmissionStatisticsReloadAll) 332 | t.Run("Users", testUsersReloadAll) 333 | t.Run("UserPrivileges", testUserPrivilegesReloadAll) 334 | } 335 | 336 | func TestSelect(t *testing.T) { 337 | t.Run("Articles", testArticlesSelect) 338 | t.Run("ArticleComments", testArticleCommentsSelect) 339 | t.Run("Contests", testContestsSelect) 340 | t.Run("ContestPlayers", testContestPlayersSelect) 341 | t.Run("ContestRanklists", testContestRanklistsSelect) 342 | t.Run("Files", testFilesSelect) 343 | t.Run("FormattedCodes", testFormattedCodesSelect) 344 | t.Run("JudgeStates", testJudgeStatesSelect) 345 | t.Run("Problems", testProblemsSelect) 346 | t.Run("ProblemTags", testProblemTagsSelect) 347 | t.Run("ProblemTagMaps", testProblemTagMapsSelect) 348 | t.Run("RatingCalculations", testRatingCalculationsSelect) 349 | t.Run("RatingHistories", testRatingHistoriesSelect) 350 | t.Run("SubmissionStatistics", testSubmissionStatisticsSelect) 351 | t.Run("Users", testUsersSelect) 352 | t.Run("UserPrivileges", testUserPrivilegesSelect) 353 | } 354 | 355 | func TestUpdate(t *testing.T) { 356 | t.Run("Articles", testArticlesUpdate) 357 | t.Run("ArticleComments", testArticleCommentsUpdate) 358 | t.Run("Contests", testContestsUpdate) 359 | t.Run("ContestPlayers", testContestPlayersUpdate) 360 | t.Run("ContestRanklists", testContestRanklistsUpdate) 361 | t.Run("Files", testFilesUpdate) 362 | t.Run("FormattedCodes", testFormattedCodesUpdate) 363 | t.Run("JudgeStates", testJudgeStatesUpdate) 364 | t.Run("Problems", testProblemsUpdate) 365 | t.Run("ProblemTags", testProblemTagsUpdate) 366 | t.Run("ProblemTagMaps", testProblemTagMapsUpdate) 367 | t.Run("RatingCalculations", testRatingCalculationsUpdate) 368 | t.Run("RatingHistories", testRatingHistoriesUpdate) 369 | t.Run("SubmissionStatistics", testSubmissionStatisticsUpdate) 370 | t.Run("Users", testUsersUpdate) 371 | t.Run("UserPrivileges", testUserPrivilegesUpdate) 372 | } 373 | 374 | func TestSliceUpdateAll(t *testing.T) { 375 | t.Run("Articles", testArticlesSliceUpdateAll) 376 | t.Run("ArticleComments", testArticleCommentsSliceUpdateAll) 377 | t.Run("Contests", testContestsSliceUpdateAll) 378 | t.Run("ContestPlayers", testContestPlayersSliceUpdateAll) 379 | t.Run("ContestRanklists", testContestRanklistsSliceUpdateAll) 380 | t.Run("Files", testFilesSliceUpdateAll) 381 | t.Run("FormattedCodes", testFormattedCodesSliceUpdateAll) 382 | t.Run("JudgeStates", testJudgeStatesSliceUpdateAll) 383 | t.Run("Problems", testProblemsSliceUpdateAll) 384 | t.Run("ProblemTags", testProblemTagsSliceUpdateAll) 385 | t.Run("ProblemTagMaps", testProblemTagMapsSliceUpdateAll) 386 | t.Run("RatingCalculations", testRatingCalculationsSliceUpdateAll) 387 | t.Run("RatingHistories", testRatingHistoriesSliceUpdateAll) 388 | t.Run("SubmissionStatistics", testSubmissionStatisticsSliceUpdateAll) 389 | t.Run("Users", testUsersSliceUpdateAll) 390 | t.Run("UserPrivileges", testUserPrivilegesSliceUpdateAll) 391 | } 392 | -------------------------------------------------------------------------------- /models/file_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/volatiletech/sqlboiler/boil" 13 | "github.com/volatiletech/sqlboiler/queries" 14 | "github.com/volatiletech/sqlboiler/randomize" 15 | "github.com/volatiletech/sqlboiler/strmangle" 16 | ) 17 | 18 | var ( 19 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 20 | // so force a package dependency in case they don't. 21 | _ = queries.Equal 22 | ) 23 | 24 | func testFiles(t *testing.T) { 25 | t.Parallel() 26 | 27 | query := Files() 28 | 29 | if query.Query == nil { 30 | t.Error("expected a query, got nothing") 31 | } 32 | } 33 | 34 | func testFilesDelete(t *testing.T) { 35 | t.Parallel() 36 | 37 | seed := randomize.NewSeed() 38 | var err error 39 | o := &File{} 40 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 41 | t.Errorf("Unable to randomize File struct: %s", err) 42 | } 43 | 44 | ctx := context.Background() 45 | tx := MustTx(boil.BeginTx(ctx, nil)) 46 | defer func() { _ = tx.Rollback() }() 47 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 48 | t.Error(err) 49 | } 50 | 51 | if rowsAff, err := o.Delete(ctx, tx); err != nil { 52 | t.Error(err) 53 | } else if rowsAff != 1 { 54 | t.Error("should only have deleted one row, but affected:", rowsAff) 55 | } 56 | 57 | count, err := Files().Count(ctx, tx) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if count != 0 { 63 | t.Error("want zero records, got:", count) 64 | } 65 | } 66 | 67 | func testFilesQueryDeleteAll(t *testing.T) { 68 | t.Parallel() 69 | 70 | seed := randomize.NewSeed() 71 | var err error 72 | o := &File{} 73 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 74 | t.Errorf("Unable to randomize File struct: %s", err) 75 | } 76 | 77 | ctx := context.Background() 78 | tx := MustTx(boil.BeginTx(ctx, nil)) 79 | defer func() { _ = tx.Rollback() }() 80 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if rowsAff, err := Files().DeleteAll(ctx, tx); err != nil { 85 | t.Error(err) 86 | } else if rowsAff != 1 { 87 | t.Error("should only have deleted one row, but affected:", rowsAff) 88 | } 89 | 90 | count, err := Files().Count(ctx, tx) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | if count != 0 { 96 | t.Error("want zero records, got:", count) 97 | } 98 | } 99 | 100 | func testFilesSliceDeleteAll(t *testing.T) { 101 | t.Parallel() 102 | 103 | seed := randomize.NewSeed() 104 | var err error 105 | o := &File{} 106 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 107 | t.Errorf("Unable to randomize File struct: %s", err) 108 | } 109 | 110 | ctx := context.Background() 111 | tx := MustTx(boil.BeginTx(ctx, nil)) 112 | defer func() { _ = tx.Rollback() }() 113 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 114 | t.Error(err) 115 | } 116 | 117 | slice := FileSlice{o} 118 | 119 | if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { 120 | t.Error(err) 121 | } else if rowsAff != 1 { 122 | t.Error("should only have deleted one row, but affected:", rowsAff) 123 | } 124 | 125 | count, err := Files().Count(ctx, tx) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | 130 | if count != 0 { 131 | t.Error("want zero records, got:", count) 132 | } 133 | } 134 | 135 | func testFilesExists(t *testing.T) { 136 | t.Parallel() 137 | 138 | seed := randomize.NewSeed() 139 | var err error 140 | o := &File{} 141 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 142 | t.Errorf("Unable to randomize File struct: %s", err) 143 | } 144 | 145 | ctx := context.Background() 146 | tx := MustTx(boil.BeginTx(ctx, nil)) 147 | defer func() { _ = tx.Rollback() }() 148 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 149 | t.Error(err) 150 | } 151 | 152 | e, err := FileExists(ctx, tx, o.ID) 153 | if err != nil { 154 | t.Errorf("Unable to check if File exists: %s", err) 155 | } 156 | if !e { 157 | t.Errorf("Expected FileExists to return true, but got false.") 158 | } 159 | } 160 | 161 | func testFilesFind(t *testing.T) { 162 | t.Parallel() 163 | 164 | seed := randomize.NewSeed() 165 | var err error 166 | o := &File{} 167 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 168 | t.Errorf("Unable to randomize File struct: %s", err) 169 | } 170 | 171 | ctx := context.Background() 172 | tx := MustTx(boil.BeginTx(ctx, nil)) 173 | defer func() { _ = tx.Rollback() }() 174 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 175 | t.Error(err) 176 | } 177 | 178 | fileFound, err := FindFile(ctx, tx, o.ID) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | 183 | if fileFound == nil { 184 | t.Error("want a record, got nil") 185 | } 186 | } 187 | 188 | func testFilesBind(t *testing.T) { 189 | t.Parallel() 190 | 191 | seed := randomize.NewSeed() 192 | var err error 193 | o := &File{} 194 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 195 | t.Errorf("Unable to randomize File struct: %s", err) 196 | } 197 | 198 | ctx := context.Background() 199 | tx := MustTx(boil.BeginTx(ctx, nil)) 200 | defer func() { _ = tx.Rollback() }() 201 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 202 | t.Error(err) 203 | } 204 | 205 | if err = Files().Bind(ctx, tx, o); err != nil { 206 | t.Error(err) 207 | } 208 | } 209 | 210 | func testFilesOne(t *testing.T) { 211 | t.Parallel() 212 | 213 | seed := randomize.NewSeed() 214 | var err error 215 | o := &File{} 216 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 217 | t.Errorf("Unable to randomize File struct: %s", err) 218 | } 219 | 220 | ctx := context.Background() 221 | tx := MustTx(boil.BeginTx(ctx, nil)) 222 | defer func() { _ = tx.Rollback() }() 223 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 224 | t.Error(err) 225 | } 226 | 227 | if x, err := Files().One(ctx, tx); err != nil { 228 | t.Error(err) 229 | } else if x == nil { 230 | t.Error("expected to get a non nil record") 231 | } 232 | } 233 | 234 | func testFilesAll(t *testing.T) { 235 | t.Parallel() 236 | 237 | seed := randomize.NewSeed() 238 | var err error 239 | fileOne := &File{} 240 | fileTwo := &File{} 241 | if err = randomize.Struct(seed, fileOne, fileDBTypes, false, fileColumnsWithDefault...); err != nil { 242 | t.Errorf("Unable to randomize File struct: %s", err) 243 | } 244 | if err = randomize.Struct(seed, fileTwo, fileDBTypes, false, fileColumnsWithDefault...); err != nil { 245 | t.Errorf("Unable to randomize File struct: %s", err) 246 | } 247 | 248 | ctx := context.Background() 249 | tx := MustTx(boil.BeginTx(ctx, nil)) 250 | defer func() { _ = tx.Rollback() }() 251 | if err = fileOne.Insert(ctx, tx, boil.Infer()); err != nil { 252 | t.Error(err) 253 | } 254 | if err = fileTwo.Insert(ctx, tx, boil.Infer()); err != nil { 255 | t.Error(err) 256 | } 257 | 258 | slice, err := Files().All(ctx, tx) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | 263 | if len(slice) != 2 { 264 | t.Error("want 2 records, got:", len(slice)) 265 | } 266 | } 267 | 268 | func testFilesCount(t *testing.T) { 269 | t.Parallel() 270 | 271 | var err error 272 | seed := randomize.NewSeed() 273 | fileOne := &File{} 274 | fileTwo := &File{} 275 | if err = randomize.Struct(seed, fileOne, fileDBTypes, false, fileColumnsWithDefault...); err != nil { 276 | t.Errorf("Unable to randomize File struct: %s", err) 277 | } 278 | if err = randomize.Struct(seed, fileTwo, fileDBTypes, false, fileColumnsWithDefault...); err != nil { 279 | t.Errorf("Unable to randomize File struct: %s", err) 280 | } 281 | 282 | ctx := context.Background() 283 | tx := MustTx(boil.BeginTx(ctx, nil)) 284 | defer func() { _ = tx.Rollback() }() 285 | if err = fileOne.Insert(ctx, tx, boil.Infer()); err != nil { 286 | t.Error(err) 287 | } 288 | if err = fileTwo.Insert(ctx, tx, boil.Infer()); err != nil { 289 | t.Error(err) 290 | } 291 | 292 | count, err := Files().Count(ctx, tx) 293 | if err != nil { 294 | t.Error(err) 295 | } 296 | 297 | if count != 2 { 298 | t.Error("want 2 records, got:", count) 299 | } 300 | } 301 | 302 | func fileBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 303 | *o = File{} 304 | return nil 305 | } 306 | 307 | func fileAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 308 | *o = File{} 309 | return nil 310 | } 311 | 312 | func fileAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 313 | *o = File{} 314 | return nil 315 | } 316 | 317 | func fileBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 318 | *o = File{} 319 | return nil 320 | } 321 | 322 | func fileAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 323 | *o = File{} 324 | return nil 325 | } 326 | 327 | func fileBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 328 | *o = File{} 329 | return nil 330 | } 331 | 332 | func fileAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 333 | *o = File{} 334 | return nil 335 | } 336 | 337 | func fileBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 338 | *o = File{} 339 | return nil 340 | } 341 | 342 | func fileAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *File) error { 343 | *o = File{} 344 | return nil 345 | } 346 | 347 | func testFilesHooks(t *testing.T) { 348 | t.Parallel() 349 | 350 | var err error 351 | 352 | ctx := context.Background() 353 | empty := &File{} 354 | o := &File{} 355 | 356 | seed := randomize.NewSeed() 357 | if err = randomize.Struct(seed, o, fileDBTypes, false); err != nil { 358 | t.Errorf("Unable to randomize File object: %s", err) 359 | } 360 | 361 | AddFileHook(boil.BeforeInsertHook, fileBeforeInsertHook) 362 | if err = o.doBeforeInsertHooks(ctx, nil); err != nil { 363 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 364 | } 365 | if !reflect.DeepEqual(o, empty) { 366 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 367 | } 368 | fileBeforeInsertHooks = []FileHook{} 369 | 370 | AddFileHook(boil.AfterInsertHook, fileAfterInsertHook) 371 | if err = o.doAfterInsertHooks(ctx, nil); err != nil { 372 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 373 | } 374 | if !reflect.DeepEqual(o, empty) { 375 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 376 | } 377 | fileAfterInsertHooks = []FileHook{} 378 | 379 | AddFileHook(boil.AfterSelectHook, fileAfterSelectHook) 380 | if err = o.doAfterSelectHooks(ctx, nil); err != nil { 381 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 382 | } 383 | if !reflect.DeepEqual(o, empty) { 384 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 385 | } 386 | fileAfterSelectHooks = []FileHook{} 387 | 388 | AddFileHook(boil.BeforeUpdateHook, fileBeforeUpdateHook) 389 | if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { 390 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 391 | } 392 | if !reflect.DeepEqual(o, empty) { 393 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 394 | } 395 | fileBeforeUpdateHooks = []FileHook{} 396 | 397 | AddFileHook(boil.AfterUpdateHook, fileAfterUpdateHook) 398 | if err = o.doAfterUpdateHooks(ctx, nil); err != nil { 399 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 400 | } 401 | if !reflect.DeepEqual(o, empty) { 402 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 403 | } 404 | fileAfterUpdateHooks = []FileHook{} 405 | 406 | AddFileHook(boil.BeforeDeleteHook, fileBeforeDeleteHook) 407 | if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { 408 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 409 | } 410 | if !reflect.DeepEqual(o, empty) { 411 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 412 | } 413 | fileBeforeDeleteHooks = []FileHook{} 414 | 415 | AddFileHook(boil.AfterDeleteHook, fileAfterDeleteHook) 416 | if err = o.doAfterDeleteHooks(ctx, nil); err != nil { 417 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 418 | } 419 | if !reflect.DeepEqual(o, empty) { 420 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 421 | } 422 | fileAfterDeleteHooks = []FileHook{} 423 | 424 | AddFileHook(boil.BeforeUpsertHook, fileBeforeUpsertHook) 425 | if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { 426 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 427 | } 428 | if !reflect.DeepEqual(o, empty) { 429 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 430 | } 431 | fileBeforeUpsertHooks = []FileHook{} 432 | 433 | AddFileHook(boil.AfterUpsertHook, fileAfterUpsertHook) 434 | if err = o.doAfterUpsertHooks(ctx, nil); err != nil { 435 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 436 | } 437 | if !reflect.DeepEqual(o, empty) { 438 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 439 | } 440 | fileAfterUpsertHooks = []FileHook{} 441 | } 442 | 443 | func testFilesInsert(t *testing.T) { 444 | t.Parallel() 445 | 446 | seed := randomize.NewSeed() 447 | var err error 448 | o := &File{} 449 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 450 | t.Errorf("Unable to randomize File struct: %s", err) 451 | } 452 | 453 | ctx := context.Background() 454 | tx := MustTx(boil.BeginTx(ctx, nil)) 455 | defer func() { _ = tx.Rollback() }() 456 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 457 | t.Error(err) 458 | } 459 | 460 | count, err := Files().Count(ctx, tx) 461 | if err != nil { 462 | t.Error(err) 463 | } 464 | 465 | if count != 1 { 466 | t.Error("want one record, got:", count) 467 | } 468 | } 469 | 470 | func testFilesInsertWhitelist(t *testing.T) { 471 | t.Parallel() 472 | 473 | seed := randomize.NewSeed() 474 | var err error 475 | o := &File{} 476 | if err = randomize.Struct(seed, o, fileDBTypes, true); err != nil { 477 | t.Errorf("Unable to randomize File struct: %s", err) 478 | } 479 | 480 | ctx := context.Background() 481 | tx := MustTx(boil.BeginTx(ctx, nil)) 482 | defer func() { _ = tx.Rollback() }() 483 | if err = o.Insert(ctx, tx, boil.Whitelist(fileColumnsWithoutDefault...)); err != nil { 484 | t.Error(err) 485 | } 486 | 487 | count, err := Files().Count(ctx, tx) 488 | if err != nil { 489 | t.Error(err) 490 | } 491 | 492 | if count != 1 { 493 | t.Error("want one record, got:", count) 494 | } 495 | } 496 | 497 | func testFilesReload(t *testing.T) { 498 | t.Parallel() 499 | 500 | seed := randomize.NewSeed() 501 | var err error 502 | o := &File{} 503 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 504 | t.Errorf("Unable to randomize File struct: %s", err) 505 | } 506 | 507 | ctx := context.Background() 508 | tx := MustTx(boil.BeginTx(ctx, nil)) 509 | defer func() { _ = tx.Rollback() }() 510 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 511 | t.Error(err) 512 | } 513 | 514 | if err = o.Reload(ctx, tx); err != nil { 515 | t.Error(err) 516 | } 517 | } 518 | 519 | func testFilesReloadAll(t *testing.T) { 520 | t.Parallel() 521 | 522 | seed := randomize.NewSeed() 523 | var err error 524 | o := &File{} 525 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 526 | t.Errorf("Unable to randomize File struct: %s", err) 527 | } 528 | 529 | ctx := context.Background() 530 | tx := MustTx(boil.BeginTx(ctx, nil)) 531 | defer func() { _ = tx.Rollback() }() 532 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 533 | t.Error(err) 534 | } 535 | 536 | slice := FileSlice{o} 537 | 538 | if err = slice.ReloadAll(ctx, tx); err != nil { 539 | t.Error(err) 540 | } 541 | } 542 | 543 | func testFilesSelect(t *testing.T) { 544 | t.Parallel() 545 | 546 | seed := randomize.NewSeed() 547 | var err error 548 | o := &File{} 549 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 550 | t.Errorf("Unable to randomize File struct: %s", err) 551 | } 552 | 553 | ctx := context.Background() 554 | tx := MustTx(boil.BeginTx(ctx, nil)) 555 | defer func() { _ = tx.Rollback() }() 556 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 557 | t.Error(err) 558 | } 559 | 560 | slice, err := Files().All(ctx, tx) 561 | if err != nil { 562 | t.Error(err) 563 | } 564 | 565 | if len(slice) != 1 { 566 | t.Error("want one record, got:", len(slice)) 567 | } 568 | } 569 | 570 | var ( 571 | fileDBTypes = map[string]string{`ID`: `int`, `Type`: `varchar`, `MD5`: `varchar`} 572 | _ = bytes.MinRead 573 | ) 574 | 575 | func testFilesUpdate(t *testing.T) { 576 | t.Parallel() 577 | 578 | if 0 == len(filePrimaryKeyColumns) { 579 | t.Skip("Skipping table with no primary key columns") 580 | } 581 | if len(fileAllColumns) == len(filePrimaryKeyColumns) { 582 | t.Skip("Skipping table with only primary key columns") 583 | } 584 | 585 | seed := randomize.NewSeed() 586 | var err error 587 | o := &File{} 588 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 589 | t.Errorf("Unable to randomize File struct: %s", err) 590 | } 591 | 592 | ctx := context.Background() 593 | tx := MustTx(boil.BeginTx(ctx, nil)) 594 | defer func() { _ = tx.Rollback() }() 595 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 596 | t.Error(err) 597 | } 598 | 599 | count, err := Files().Count(ctx, tx) 600 | if err != nil { 601 | t.Error(err) 602 | } 603 | 604 | if count != 1 { 605 | t.Error("want one record, got:", count) 606 | } 607 | 608 | if err = randomize.Struct(seed, o, fileDBTypes, true, filePrimaryKeyColumns...); err != nil { 609 | t.Errorf("Unable to randomize File struct: %s", err) 610 | } 611 | 612 | if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { 613 | t.Error(err) 614 | } else if rowsAff != 1 { 615 | t.Error("should only affect one row but affected", rowsAff) 616 | } 617 | } 618 | 619 | func testFilesSliceUpdateAll(t *testing.T) { 620 | t.Parallel() 621 | 622 | if len(fileAllColumns) == len(filePrimaryKeyColumns) { 623 | t.Skip("Skipping table with only primary key columns") 624 | } 625 | 626 | seed := randomize.NewSeed() 627 | var err error 628 | o := &File{} 629 | if err = randomize.Struct(seed, o, fileDBTypes, true, fileColumnsWithDefault...); err != nil { 630 | t.Errorf("Unable to randomize File struct: %s", err) 631 | } 632 | 633 | ctx := context.Background() 634 | tx := MustTx(boil.BeginTx(ctx, nil)) 635 | defer func() { _ = tx.Rollback() }() 636 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 637 | t.Error(err) 638 | } 639 | 640 | count, err := Files().Count(ctx, tx) 641 | if err != nil { 642 | t.Error(err) 643 | } 644 | 645 | if count != 1 { 646 | t.Error("want one record, got:", count) 647 | } 648 | 649 | if err = randomize.Struct(seed, o, fileDBTypes, true, filePrimaryKeyColumns...); err != nil { 650 | t.Errorf("Unable to randomize File struct: %s", err) 651 | } 652 | 653 | // Remove Primary keys and unique columns from what we plan to update 654 | var fields []string 655 | if strmangle.StringSliceMatch(fileAllColumns, filePrimaryKeyColumns) { 656 | fields = fileAllColumns 657 | } else { 658 | fields = strmangle.SetComplement( 659 | fileAllColumns, 660 | filePrimaryKeyColumns, 661 | ) 662 | } 663 | 664 | value := reflect.Indirect(reflect.ValueOf(o)) 665 | typ := reflect.TypeOf(o).Elem() 666 | n := typ.NumField() 667 | 668 | updateMap := M{} 669 | for _, col := range fields { 670 | for i := 0; i < n; i++ { 671 | f := typ.Field(i) 672 | if f.Tag.Get("boil") == col { 673 | updateMap[col] = value.Field(i).Interface() 674 | } 675 | } 676 | } 677 | 678 | slice := FileSlice{o} 679 | if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { 680 | t.Error(err) 681 | } else if rowsAff != 1 { 682 | t.Error("wanted one record updated but got", rowsAff) 683 | } 684 | } 685 | 686 | func testFilesUpsert(t *testing.T) { 687 | t.Parallel() 688 | 689 | if len(fileAllColumns) == len(filePrimaryKeyColumns) { 690 | t.Skip("Skipping table with only primary key columns") 691 | } 692 | if len(mySQLFileUniqueColumns) == 0 { 693 | t.Skip("Skipping table with no unique columns to conflict on") 694 | } 695 | 696 | seed := randomize.NewSeed() 697 | var err error 698 | // Attempt the INSERT side of an UPSERT 699 | o := File{} 700 | if err = randomize.Struct(seed, &o, fileDBTypes, false); err != nil { 701 | t.Errorf("Unable to randomize File struct: %s", err) 702 | } 703 | 704 | ctx := context.Background() 705 | tx := MustTx(boil.BeginTx(ctx, nil)) 706 | defer func() { _ = tx.Rollback() }() 707 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 708 | t.Errorf("Unable to upsert File: %s", err) 709 | } 710 | 711 | count, err := Files().Count(ctx, tx) 712 | if err != nil { 713 | t.Error(err) 714 | } 715 | if count != 1 { 716 | t.Error("want one record, got:", count) 717 | } 718 | 719 | // Attempt the UPDATE side of an UPSERT 720 | if err = randomize.Struct(seed, &o, fileDBTypes, false, filePrimaryKeyColumns...); err != nil { 721 | t.Errorf("Unable to randomize File struct: %s", err) 722 | } 723 | 724 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 725 | t.Errorf("Unable to upsert File: %s", err) 726 | } 727 | 728 | count, err = Files().Count(ctx, tx) 729 | if err != nil { 730 | t.Error(err) 731 | } 732 | if count != 1 { 733 | t.Error("want one record, got:", count) 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /models/user_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/volatiletech/sqlboiler/boil" 13 | "github.com/volatiletech/sqlboiler/queries" 14 | "github.com/volatiletech/sqlboiler/randomize" 15 | "github.com/volatiletech/sqlboiler/strmangle" 16 | ) 17 | 18 | var ( 19 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 20 | // so force a package dependency in case they don't. 21 | _ = queries.Equal 22 | ) 23 | 24 | func testUsers(t *testing.T) { 25 | t.Parallel() 26 | 27 | query := Users() 28 | 29 | if query.Query == nil { 30 | t.Error("expected a query, got nothing") 31 | } 32 | } 33 | 34 | func testUsersDelete(t *testing.T) { 35 | t.Parallel() 36 | 37 | seed := randomize.NewSeed() 38 | var err error 39 | o := &User{} 40 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 41 | t.Errorf("Unable to randomize User struct: %s", err) 42 | } 43 | 44 | ctx := context.Background() 45 | tx := MustTx(boil.BeginTx(ctx, nil)) 46 | defer func() { _ = tx.Rollback() }() 47 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 48 | t.Error(err) 49 | } 50 | 51 | if rowsAff, err := o.Delete(ctx, tx); err != nil { 52 | t.Error(err) 53 | } else if rowsAff != 1 { 54 | t.Error("should only have deleted one row, but affected:", rowsAff) 55 | } 56 | 57 | count, err := Users().Count(ctx, tx) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if count != 0 { 63 | t.Error("want zero records, got:", count) 64 | } 65 | } 66 | 67 | func testUsersQueryDeleteAll(t *testing.T) { 68 | t.Parallel() 69 | 70 | seed := randomize.NewSeed() 71 | var err error 72 | o := &User{} 73 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 74 | t.Errorf("Unable to randomize User struct: %s", err) 75 | } 76 | 77 | ctx := context.Background() 78 | tx := MustTx(boil.BeginTx(ctx, nil)) 79 | defer func() { _ = tx.Rollback() }() 80 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if rowsAff, err := Users().DeleteAll(ctx, tx); err != nil { 85 | t.Error(err) 86 | } else if rowsAff != 1 { 87 | t.Error("should only have deleted one row, but affected:", rowsAff) 88 | } 89 | 90 | count, err := Users().Count(ctx, tx) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | if count != 0 { 96 | t.Error("want zero records, got:", count) 97 | } 98 | } 99 | 100 | func testUsersSliceDeleteAll(t *testing.T) { 101 | t.Parallel() 102 | 103 | seed := randomize.NewSeed() 104 | var err error 105 | o := &User{} 106 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 107 | t.Errorf("Unable to randomize User struct: %s", err) 108 | } 109 | 110 | ctx := context.Background() 111 | tx := MustTx(boil.BeginTx(ctx, nil)) 112 | defer func() { _ = tx.Rollback() }() 113 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 114 | t.Error(err) 115 | } 116 | 117 | slice := UserSlice{o} 118 | 119 | if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { 120 | t.Error(err) 121 | } else if rowsAff != 1 { 122 | t.Error("should only have deleted one row, but affected:", rowsAff) 123 | } 124 | 125 | count, err := Users().Count(ctx, tx) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | 130 | if count != 0 { 131 | t.Error("want zero records, got:", count) 132 | } 133 | } 134 | 135 | func testUsersExists(t *testing.T) { 136 | t.Parallel() 137 | 138 | seed := randomize.NewSeed() 139 | var err error 140 | o := &User{} 141 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 142 | t.Errorf("Unable to randomize User struct: %s", err) 143 | } 144 | 145 | ctx := context.Background() 146 | tx := MustTx(boil.BeginTx(ctx, nil)) 147 | defer func() { _ = tx.Rollback() }() 148 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 149 | t.Error(err) 150 | } 151 | 152 | e, err := UserExists(ctx, tx, o.ID) 153 | if err != nil { 154 | t.Errorf("Unable to check if User exists: %s", err) 155 | } 156 | if !e { 157 | t.Errorf("Expected UserExists to return true, but got false.") 158 | } 159 | } 160 | 161 | func testUsersFind(t *testing.T) { 162 | t.Parallel() 163 | 164 | seed := randomize.NewSeed() 165 | var err error 166 | o := &User{} 167 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 168 | t.Errorf("Unable to randomize User struct: %s", err) 169 | } 170 | 171 | ctx := context.Background() 172 | tx := MustTx(boil.BeginTx(ctx, nil)) 173 | defer func() { _ = tx.Rollback() }() 174 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 175 | t.Error(err) 176 | } 177 | 178 | userFound, err := FindUser(ctx, tx, o.ID) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | 183 | if userFound == nil { 184 | t.Error("want a record, got nil") 185 | } 186 | } 187 | 188 | func testUsersBind(t *testing.T) { 189 | t.Parallel() 190 | 191 | seed := randomize.NewSeed() 192 | var err error 193 | o := &User{} 194 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 195 | t.Errorf("Unable to randomize User struct: %s", err) 196 | } 197 | 198 | ctx := context.Background() 199 | tx := MustTx(boil.BeginTx(ctx, nil)) 200 | defer func() { _ = tx.Rollback() }() 201 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 202 | t.Error(err) 203 | } 204 | 205 | if err = Users().Bind(ctx, tx, o); err != nil { 206 | t.Error(err) 207 | } 208 | } 209 | 210 | func testUsersOne(t *testing.T) { 211 | t.Parallel() 212 | 213 | seed := randomize.NewSeed() 214 | var err error 215 | o := &User{} 216 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 217 | t.Errorf("Unable to randomize User struct: %s", err) 218 | } 219 | 220 | ctx := context.Background() 221 | tx := MustTx(boil.BeginTx(ctx, nil)) 222 | defer func() { _ = tx.Rollback() }() 223 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 224 | t.Error(err) 225 | } 226 | 227 | if x, err := Users().One(ctx, tx); err != nil { 228 | t.Error(err) 229 | } else if x == nil { 230 | t.Error("expected to get a non nil record") 231 | } 232 | } 233 | 234 | func testUsersAll(t *testing.T) { 235 | t.Parallel() 236 | 237 | seed := randomize.NewSeed() 238 | var err error 239 | userOne := &User{} 240 | userTwo := &User{} 241 | if err = randomize.Struct(seed, userOne, userDBTypes, false, userColumnsWithDefault...); err != nil { 242 | t.Errorf("Unable to randomize User struct: %s", err) 243 | } 244 | if err = randomize.Struct(seed, userTwo, userDBTypes, false, userColumnsWithDefault...); err != nil { 245 | t.Errorf("Unable to randomize User struct: %s", err) 246 | } 247 | 248 | ctx := context.Background() 249 | tx := MustTx(boil.BeginTx(ctx, nil)) 250 | defer func() { _ = tx.Rollback() }() 251 | if err = userOne.Insert(ctx, tx, boil.Infer()); err != nil { 252 | t.Error(err) 253 | } 254 | if err = userTwo.Insert(ctx, tx, boil.Infer()); err != nil { 255 | t.Error(err) 256 | } 257 | 258 | slice, err := Users().All(ctx, tx) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | 263 | if len(slice) != 2 { 264 | t.Error("want 2 records, got:", len(slice)) 265 | } 266 | } 267 | 268 | func testUsersCount(t *testing.T) { 269 | t.Parallel() 270 | 271 | var err error 272 | seed := randomize.NewSeed() 273 | userOne := &User{} 274 | userTwo := &User{} 275 | if err = randomize.Struct(seed, userOne, userDBTypes, false, userColumnsWithDefault...); err != nil { 276 | t.Errorf("Unable to randomize User struct: %s", err) 277 | } 278 | if err = randomize.Struct(seed, userTwo, userDBTypes, false, userColumnsWithDefault...); err != nil { 279 | t.Errorf("Unable to randomize User struct: %s", err) 280 | } 281 | 282 | ctx := context.Background() 283 | tx := MustTx(boil.BeginTx(ctx, nil)) 284 | defer func() { _ = tx.Rollback() }() 285 | if err = userOne.Insert(ctx, tx, boil.Infer()); err != nil { 286 | t.Error(err) 287 | } 288 | if err = userTwo.Insert(ctx, tx, boil.Infer()); err != nil { 289 | t.Error(err) 290 | } 291 | 292 | count, err := Users().Count(ctx, tx) 293 | if err != nil { 294 | t.Error(err) 295 | } 296 | 297 | if count != 2 { 298 | t.Error("want 2 records, got:", count) 299 | } 300 | } 301 | 302 | func userBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 303 | *o = User{} 304 | return nil 305 | } 306 | 307 | func userAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 308 | *o = User{} 309 | return nil 310 | } 311 | 312 | func userAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 313 | *o = User{} 314 | return nil 315 | } 316 | 317 | func userBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 318 | *o = User{} 319 | return nil 320 | } 321 | 322 | func userAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 323 | *o = User{} 324 | return nil 325 | } 326 | 327 | func userBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 328 | *o = User{} 329 | return nil 330 | } 331 | 332 | func userAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 333 | *o = User{} 334 | return nil 335 | } 336 | 337 | func userBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 338 | *o = User{} 339 | return nil 340 | } 341 | 342 | func userAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *User) error { 343 | *o = User{} 344 | return nil 345 | } 346 | 347 | func testUsersHooks(t *testing.T) { 348 | t.Parallel() 349 | 350 | var err error 351 | 352 | ctx := context.Background() 353 | empty := &User{} 354 | o := &User{} 355 | 356 | seed := randomize.NewSeed() 357 | if err = randomize.Struct(seed, o, userDBTypes, false); err != nil { 358 | t.Errorf("Unable to randomize User object: %s", err) 359 | } 360 | 361 | AddUserHook(boil.BeforeInsertHook, userBeforeInsertHook) 362 | if err = o.doBeforeInsertHooks(ctx, nil); err != nil { 363 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 364 | } 365 | if !reflect.DeepEqual(o, empty) { 366 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 367 | } 368 | userBeforeInsertHooks = []UserHook{} 369 | 370 | AddUserHook(boil.AfterInsertHook, userAfterInsertHook) 371 | if err = o.doAfterInsertHooks(ctx, nil); err != nil { 372 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 373 | } 374 | if !reflect.DeepEqual(o, empty) { 375 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 376 | } 377 | userAfterInsertHooks = []UserHook{} 378 | 379 | AddUserHook(boil.AfterSelectHook, userAfterSelectHook) 380 | if err = o.doAfterSelectHooks(ctx, nil); err != nil { 381 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 382 | } 383 | if !reflect.DeepEqual(o, empty) { 384 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 385 | } 386 | userAfterSelectHooks = []UserHook{} 387 | 388 | AddUserHook(boil.BeforeUpdateHook, userBeforeUpdateHook) 389 | if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { 390 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 391 | } 392 | if !reflect.DeepEqual(o, empty) { 393 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 394 | } 395 | userBeforeUpdateHooks = []UserHook{} 396 | 397 | AddUserHook(boil.AfterUpdateHook, userAfterUpdateHook) 398 | if err = o.doAfterUpdateHooks(ctx, nil); err != nil { 399 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 400 | } 401 | if !reflect.DeepEqual(o, empty) { 402 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 403 | } 404 | userAfterUpdateHooks = []UserHook{} 405 | 406 | AddUserHook(boil.BeforeDeleteHook, userBeforeDeleteHook) 407 | if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { 408 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 409 | } 410 | if !reflect.DeepEqual(o, empty) { 411 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 412 | } 413 | userBeforeDeleteHooks = []UserHook{} 414 | 415 | AddUserHook(boil.AfterDeleteHook, userAfterDeleteHook) 416 | if err = o.doAfterDeleteHooks(ctx, nil); err != nil { 417 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 418 | } 419 | if !reflect.DeepEqual(o, empty) { 420 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 421 | } 422 | userAfterDeleteHooks = []UserHook{} 423 | 424 | AddUserHook(boil.BeforeUpsertHook, userBeforeUpsertHook) 425 | if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { 426 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 427 | } 428 | if !reflect.DeepEqual(o, empty) { 429 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 430 | } 431 | userBeforeUpsertHooks = []UserHook{} 432 | 433 | AddUserHook(boil.AfterUpsertHook, userAfterUpsertHook) 434 | if err = o.doAfterUpsertHooks(ctx, nil); err != nil { 435 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 436 | } 437 | if !reflect.DeepEqual(o, empty) { 438 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 439 | } 440 | userAfterUpsertHooks = []UserHook{} 441 | } 442 | 443 | func testUsersInsert(t *testing.T) { 444 | t.Parallel() 445 | 446 | seed := randomize.NewSeed() 447 | var err error 448 | o := &User{} 449 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 450 | t.Errorf("Unable to randomize User struct: %s", err) 451 | } 452 | 453 | ctx := context.Background() 454 | tx := MustTx(boil.BeginTx(ctx, nil)) 455 | defer func() { _ = tx.Rollback() }() 456 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 457 | t.Error(err) 458 | } 459 | 460 | count, err := Users().Count(ctx, tx) 461 | if err != nil { 462 | t.Error(err) 463 | } 464 | 465 | if count != 1 { 466 | t.Error("want one record, got:", count) 467 | } 468 | } 469 | 470 | func testUsersInsertWhitelist(t *testing.T) { 471 | t.Parallel() 472 | 473 | seed := randomize.NewSeed() 474 | var err error 475 | o := &User{} 476 | if err = randomize.Struct(seed, o, userDBTypes, true); err != nil { 477 | t.Errorf("Unable to randomize User struct: %s", err) 478 | } 479 | 480 | ctx := context.Background() 481 | tx := MustTx(boil.BeginTx(ctx, nil)) 482 | defer func() { _ = tx.Rollback() }() 483 | if err = o.Insert(ctx, tx, boil.Whitelist(userColumnsWithoutDefault...)); err != nil { 484 | t.Error(err) 485 | } 486 | 487 | count, err := Users().Count(ctx, tx) 488 | if err != nil { 489 | t.Error(err) 490 | } 491 | 492 | if count != 1 { 493 | t.Error("want one record, got:", count) 494 | } 495 | } 496 | 497 | func testUsersReload(t *testing.T) { 498 | t.Parallel() 499 | 500 | seed := randomize.NewSeed() 501 | var err error 502 | o := &User{} 503 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 504 | t.Errorf("Unable to randomize User struct: %s", err) 505 | } 506 | 507 | ctx := context.Background() 508 | tx := MustTx(boil.BeginTx(ctx, nil)) 509 | defer func() { _ = tx.Rollback() }() 510 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 511 | t.Error(err) 512 | } 513 | 514 | if err = o.Reload(ctx, tx); err != nil { 515 | t.Error(err) 516 | } 517 | } 518 | 519 | func testUsersReloadAll(t *testing.T) { 520 | t.Parallel() 521 | 522 | seed := randomize.NewSeed() 523 | var err error 524 | o := &User{} 525 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 526 | t.Errorf("Unable to randomize User struct: %s", err) 527 | } 528 | 529 | ctx := context.Background() 530 | tx := MustTx(boil.BeginTx(ctx, nil)) 531 | defer func() { _ = tx.Rollback() }() 532 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 533 | t.Error(err) 534 | } 535 | 536 | slice := UserSlice{o} 537 | 538 | if err = slice.ReloadAll(ctx, tx); err != nil { 539 | t.Error(err) 540 | } 541 | } 542 | 543 | func testUsersSelect(t *testing.T) { 544 | t.Parallel() 545 | 546 | seed := randomize.NewSeed() 547 | var err error 548 | o := &User{} 549 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 550 | t.Errorf("Unable to randomize User struct: %s", err) 551 | } 552 | 553 | ctx := context.Background() 554 | tx := MustTx(boil.BeginTx(ctx, nil)) 555 | defer func() { _ = tx.Rollback() }() 556 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 557 | t.Error(err) 558 | } 559 | 560 | slice, err := Users().All(ctx, tx) 561 | if err != nil { 562 | t.Error(err) 563 | } 564 | 565 | if len(slice) != 1 { 566 | t.Error("want one record, got:", len(slice)) 567 | } 568 | } 569 | 570 | var ( 571 | userDBTypes = map[string]string{`ID`: `int`, `Username`: `varchar`, `Email`: `varchar`, `Password`: `varchar`, `Nickname`: `varchar`, `Nameplate`: `text`, `Information`: `text`, `AcNum`: `int`, `SubmitNum`: `int`, `IsAdmin`: `tinyint`, `IsShow`: `tinyint`, `PublicEmail`: `tinyint`, `PreferFormattedCode`: `tinyint`, `Sex`: `int`, `Rating`: `int`, `RegisterTime`: `int`} 572 | _ = bytes.MinRead 573 | ) 574 | 575 | func testUsersUpdate(t *testing.T) { 576 | t.Parallel() 577 | 578 | if 0 == len(userPrimaryKeyColumns) { 579 | t.Skip("Skipping table with no primary key columns") 580 | } 581 | if len(userAllColumns) == len(userPrimaryKeyColumns) { 582 | t.Skip("Skipping table with only primary key columns") 583 | } 584 | 585 | seed := randomize.NewSeed() 586 | var err error 587 | o := &User{} 588 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 589 | t.Errorf("Unable to randomize User struct: %s", err) 590 | } 591 | 592 | ctx := context.Background() 593 | tx := MustTx(boil.BeginTx(ctx, nil)) 594 | defer func() { _ = tx.Rollback() }() 595 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 596 | t.Error(err) 597 | } 598 | 599 | count, err := Users().Count(ctx, tx) 600 | if err != nil { 601 | t.Error(err) 602 | } 603 | 604 | if count != 1 { 605 | t.Error("want one record, got:", count) 606 | } 607 | 608 | if err = randomize.Struct(seed, o, userDBTypes, true, userPrimaryKeyColumns...); err != nil { 609 | t.Errorf("Unable to randomize User struct: %s", err) 610 | } 611 | 612 | if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { 613 | t.Error(err) 614 | } else if rowsAff != 1 { 615 | t.Error("should only affect one row but affected", rowsAff) 616 | } 617 | } 618 | 619 | func testUsersSliceUpdateAll(t *testing.T) { 620 | t.Parallel() 621 | 622 | if len(userAllColumns) == len(userPrimaryKeyColumns) { 623 | t.Skip("Skipping table with only primary key columns") 624 | } 625 | 626 | seed := randomize.NewSeed() 627 | var err error 628 | o := &User{} 629 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 630 | t.Errorf("Unable to randomize User struct: %s", err) 631 | } 632 | 633 | ctx := context.Background() 634 | tx := MustTx(boil.BeginTx(ctx, nil)) 635 | defer func() { _ = tx.Rollback() }() 636 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 637 | t.Error(err) 638 | } 639 | 640 | count, err := Users().Count(ctx, tx) 641 | if err != nil { 642 | t.Error(err) 643 | } 644 | 645 | if count != 1 { 646 | t.Error("want one record, got:", count) 647 | } 648 | 649 | if err = randomize.Struct(seed, o, userDBTypes, true, userPrimaryKeyColumns...); err != nil { 650 | t.Errorf("Unable to randomize User struct: %s", err) 651 | } 652 | 653 | // Remove Primary keys and unique columns from what we plan to update 654 | var fields []string 655 | if strmangle.StringSliceMatch(userAllColumns, userPrimaryKeyColumns) { 656 | fields = userAllColumns 657 | } else { 658 | fields = strmangle.SetComplement( 659 | userAllColumns, 660 | userPrimaryKeyColumns, 661 | ) 662 | } 663 | 664 | value := reflect.Indirect(reflect.ValueOf(o)) 665 | typ := reflect.TypeOf(o).Elem() 666 | n := typ.NumField() 667 | 668 | updateMap := M{} 669 | for _, col := range fields { 670 | for i := 0; i < n; i++ { 671 | f := typ.Field(i) 672 | if f.Tag.Get("boil") == col { 673 | updateMap[col] = value.Field(i).Interface() 674 | } 675 | } 676 | } 677 | 678 | slice := UserSlice{o} 679 | if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { 680 | t.Error(err) 681 | } else if rowsAff != 1 { 682 | t.Error("wanted one record updated but got", rowsAff) 683 | } 684 | } 685 | 686 | func testUsersUpsert(t *testing.T) { 687 | t.Parallel() 688 | 689 | if len(userAllColumns) == len(userPrimaryKeyColumns) { 690 | t.Skip("Skipping table with only primary key columns") 691 | } 692 | if len(mySQLUserUniqueColumns) == 0 { 693 | t.Skip("Skipping table with no unique columns to conflict on") 694 | } 695 | 696 | seed := randomize.NewSeed() 697 | var err error 698 | // Attempt the INSERT side of an UPSERT 699 | o := User{} 700 | if err = randomize.Struct(seed, &o, userDBTypes, false); err != nil { 701 | t.Errorf("Unable to randomize User struct: %s", err) 702 | } 703 | 704 | ctx := context.Background() 705 | tx := MustTx(boil.BeginTx(ctx, nil)) 706 | defer func() { _ = tx.Rollback() }() 707 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 708 | t.Errorf("Unable to upsert User: %s", err) 709 | } 710 | 711 | count, err := Users().Count(ctx, tx) 712 | if err != nil { 713 | t.Error(err) 714 | } 715 | if count != 1 { 716 | t.Error("want one record, got:", count) 717 | } 718 | 719 | // Attempt the UPDATE side of an UPSERT 720 | if err = randomize.Struct(seed, &o, userDBTypes, false, userPrimaryKeyColumns...); err != nil { 721 | t.Errorf("Unable to randomize User struct: %s", err) 722 | } 723 | 724 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 725 | t.Errorf("Unable to upsert User: %s", err) 726 | } 727 | 728 | count, err = Users().Count(ctx, tx) 729 | if err != nil { 730 | t.Error(err) 731 | } 732 | if count != 1 { 733 | t.Error("want one record, got:", count) 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /models/article_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/volatiletech/sqlboiler/boil" 13 | "github.com/volatiletech/sqlboiler/queries" 14 | "github.com/volatiletech/sqlboiler/randomize" 15 | "github.com/volatiletech/sqlboiler/strmangle" 16 | ) 17 | 18 | var ( 19 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 20 | // so force a package dependency in case they don't. 21 | _ = queries.Equal 22 | ) 23 | 24 | func testArticles(t *testing.T) { 25 | t.Parallel() 26 | 27 | query := Articles() 28 | 29 | if query.Query == nil { 30 | t.Error("expected a query, got nothing") 31 | } 32 | } 33 | 34 | func testArticlesDelete(t *testing.T) { 35 | t.Parallel() 36 | 37 | seed := randomize.NewSeed() 38 | var err error 39 | o := &Article{} 40 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 41 | t.Errorf("Unable to randomize Article struct: %s", err) 42 | } 43 | 44 | ctx := context.Background() 45 | tx := MustTx(boil.BeginTx(ctx, nil)) 46 | defer func() { _ = tx.Rollback() }() 47 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 48 | t.Error(err) 49 | } 50 | 51 | if rowsAff, err := o.Delete(ctx, tx); err != nil { 52 | t.Error(err) 53 | } else if rowsAff != 1 { 54 | t.Error("should only have deleted one row, but affected:", rowsAff) 55 | } 56 | 57 | count, err := Articles().Count(ctx, tx) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if count != 0 { 63 | t.Error("want zero records, got:", count) 64 | } 65 | } 66 | 67 | func testArticlesQueryDeleteAll(t *testing.T) { 68 | t.Parallel() 69 | 70 | seed := randomize.NewSeed() 71 | var err error 72 | o := &Article{} 73 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 74 | t.Errorf("Unable to randomize Article struct: %s", err) 75 | } 76 | 77 | ctx := context.Background() 78 | tx := MustTx(boil.BeginTx(ctx, nil)) 79 | defer func() { _ = tx.Rollback() }() 80 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if rowsAff, err := Articles().DeleteAll(ctx, tx); err != nil { 85 | t.Error(err) 86 | } else if rowsAff != 1 { 87 | t.Error("should only have deleted one row, but affected:", rowsAff) 88 | } 89 | 90 | count, err := Articles().Count(ctx, tx) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | if count != 0 { 96 | t.Error("want zero records, got:", count) 97 | } 98 | } 99 | 100 | func testArticlesSliceDeleteAll(t *testing.T) { 101 | t.Parallel() 102 | 103 | seed := randomize.NewSeed() 104 | var err error 105 | o := &Article{} 106 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 107 | t.Errorf("Unable to randomize Article struct: %s", err) 108 | } 109 | 110 | ctx := context.Background() 111 | tx := MustTx(boil.BeginTx(ctx, nil)) 112 | defer func() { _ = tx.Rollback() }() 113 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 114 | t.Error(err) 115 | } 116 | 117 | slice := ArticleSlice{o} 118 | 119 | if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { 120 | t.Error(err) 121 | } else if rowsAff != 1 { 122 | t.Error("should only have deleted one row, but affected:", rowsAff) 123 | } 124 | 125 | count, err := Articles().Count(ctx, tx) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | 130 | if count != 0 { 131 | t.Error("want zero records, got:", count) 132 | } 133 | } 134 | 135 | func testArticlesExists(t *testing.T) { 136 | t.Parallel() 137 | 138 | seed := randomize.NewSeed() 139 | var err error 140 | o := &Article{} 141 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 142 | t.Errorf("Unable to randomize Article struct: %s", err) 143 | } 144 | 145 | ctx := context.Background() 146 | tx := MustTx(boil.BeginTx(ctx, nil)) 147 | defer func() { _ = tx.Rollback() }() 148 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 149 | t.Error(err) 150 | } 151 | 152 | e, err := ArticleExists(ctx, tx, o.ID) 153 | if err != nil { 154 | t.Errorf("Unable to check if Article exists: %s", err) 155 | } 156 | if !e { 157 | t.Errorf("Expected ArticleExists to return true, but got false.") 158 | } 159 | } 160 | 161 | func testArticlesFind(t *testing.T) { 162 | t.Parallel() 163 | 164 | seed := randomize.NewSeed() 165 | var err error 166 | o := &Article{} 167 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 168 | t.Errorf("Unable to randomize Article struct: %s", err) 169 | } 170 | 171 | ctx := context.Background() 172 | tx := MustTx(boil.BeginTx(ctx, nil)) 173 | defer func() { _ = tx.Rollback() }() 174 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 175 | t.Error(err) 176 | } 177 | 178 | articleFound, err := FindArticle(ctx, tx, o.ID) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | 183 | if articleFound == nil { 184 | t.Error("want a record, got nil") 185 | } 186 | } 187 | 188 | func testArticlesBind(t *testing.T) { 189 | t.Parallel() 190 | 191 | seed := randomize.NewSeed() 192 | var err error 193 | o := &Article{} 194 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 195 | t.Errorf("Unable to randomize Article struct: %s", err) 196 | } 197 | 198 | ctx := context.Background() 199 | tx := MustTx(boil.BeginTx(ctx, nil)) 200 | defer func() { _ = tx.Rollback() }() 201 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 202 | t.Error(err) 203 | } 204 | 205 | if err = Articles().Bind(ctx, tx, o); err != nil { 206 | t.Error(err) 207 | } 208 | } 209 | 210 | func testArticlesOne(t *testing.T) { 211 | t.Parallel() 212 | 213 | seed := randomize.NewSeed() 214 | var err error 215 | o := &Article{} 216 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 217 | t.Errorf("Unable to randomize Article struct: %s", err) 218 | } 219 | 220 | ctx := context.Background() 221 | tx := MustTx(boil.BeginTx(ctx, nil)) 222 | defer func() { _ = tx.Rollback() }() 223 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 224 | t.Error(err) 225 | } 226 | 227 | if x, err := Articles().One(ctx, tx); err != nil { 228 | t.Error(err) 229 | } else if x == nil { 230 | t.Error("expected to get a non nil record") 231 | } 232 | } 233 | 234 | func testArticlesAll(t *testing.T) { 235 | t.Parallel() 236 | 237 | seed := randomize.NewSeed() 238 | var err error 239 | articleOne := &Article{} 240 | articleTwo := &Article{} 241 | if err = randomize.Struct(seed, articleOne, articleDBTypes, false, articleColumnsWithDefault...); err != nil { 242 | t.Errorf("Unable to randomize Article struct: %s", err) 243 | } 244 | if err = randomize.Struct(seed, articleTwo, articleDBTypes, false, articleColumnsWithDefault...); err != nil { 245 | t.Errorf("Unable to randomize Article struct: %s", err) 246 | } 247 | 248 | ctx := context.Background() 249 | tx := MustTx(boil.BeginTx(ctx, nil)) 250 | defer func() { _ = tx.Rollback() }() 251 | if err = articleOne.Insert(ctx, tx, boil.Infer()); err != nil { 252 | t.Error(err) 253 | } 254 | if err = articleTwo.Insert(ctx, tx, boil.Infer()); err != nil { 255 | t.Error(err) 256 | } 257 | 258 | slice, err := Articles().All(ctx, tx) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | 263 | if len(slice) != 2 { 264 | t.Error("want 2 records, got:", len(slice)) 265 | } 266 | } 267 | 268 | func testArticlesCount(t *testing.T) { 269 | t.Parallel() 270 | 271 | var err error 272 | seed := randomize.NewSeed() 273 | articleOne := &Article{} 274 | articleTwo := &Article{} 275 | if err = randomize.Struct(seed, articleOne, articleDBTypes, false, articleColumnsWithDefault...); err != nil { 276 | t.Errorf("Unable to randomize Article struct: %s", err) 277 | } 278 | if err = randomize.Struct(seed, articleTwo, articleDBTypes, false, articleColumnsWithDefault...); err != nil { 279 | t.Errorf("Unable to randomize Article struct: %s", err) 280 | } 281 | 282 | ctx := context.Background() 283 | tx := MustTx(boil.BeginTx(ctx, nil)) 284 | defer func() { _ = tx.Rollback() }() 285 | if err = articleOne.Insert(ctx, tx, boil.Infer()); err != nil { 286 | t.Error(err) 287 | } 288 | if err = articleTwo.Insert(ctx, tx, boil.Infer()); err != nil { 289 | t.Error(err) 290 | } 291 | 292 | count, err := Articles().Count(ctx, tx) 293 | if err != nil { 294 | t.Error(err) 295 | } 296 | 297 | if count != 2 { 298 | t.Error("want 2 records, got:", count) 299 | } 300 | } 301 | 302 | func articleBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 303 | *o = Article{} 304 | return nil 305 | } 306 | 307 | func articleAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 308 | *o = Article{} 309 | return nil 310 | } 311 | 312 | func articleAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 313 | *o = Article{} 314 | return nil 315 | } 316 | 317 | func articleBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 318 | *o = Article{} 319 | return nil 320 | } 321 | 322 | func articleAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 323 | *o = Article{} 324 | return nil 325 | } 326 | 327 | func articleBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 328 | *o = Article{} 329 | return nil 330 | } 331 | 332 | func articleAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 333 | *o = Article{} 334 | return nil 335 | } 336 | 337 | func articleBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 338 | *o = Article{} 339 | return nil 340 | } 341 | 342 | func articleAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *Article) error { 343 | *o = Article{} 344 | return nil 345 | } 346 | 347 | func testArticlesHooks(t *testing.T) { 348 | t.Parallel() 349 | 350 | var err error 351 | 352 | ctx := context.Background() 353 | empty := &Article{} 354 | o := &Article{} 355 | 356 | seed := randomize.NewSeed() 357 | if err = randomize.Struct(seed, o, articleDBTypes, false); err != nil { 358 | t.Errorf("Unable to randomize Article object: %s", err) 359 | } 360 | 361 | AddArticleHook(boil.BeforeInsertHook, articleBeforeInsertHook) 362 | if err = o.doBeforeInsertHooks(ctx, nil); err != nil { 363 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 364 | } 365 | if !reflect.DeepEqual(o, empty) { 366 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 367 | } 368 | articleBeforeInsertHooks = []ArticleHook{} 369 | 370 | AddArticleHook(boil.AfterInsertHook, articleAfterInsertHook) 371 | if err = o.doAfterInsertHooks(ctx, nil); err != nil { 372 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 373 | } 374 | if !reflect.DeepEqual(o, empty) { 375 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 376 | } 377 | articleAfterInsertHooks = []ArticleHook{} 378 | 379 | AddArticleHook(boil.AfterSelectHook, articleAfterSelectHook) 380 | if err = o.doAfterSelectHooks(ctx, nil); err != nil { 381 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 382 | } 383 | if !reflect.DeepEqual(o, empty) { 384 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 385 | } 386 | articleAfterSelectHooks = []ArticleHook{} 387 | 388 | AddArticleHook(boil.BeforeUpdateHook, articleBeforeUpdateHook) 389 | if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { 390 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 391 | } 392 | if !reflect.DeepEqual(o, empty) { 393 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 394 | } 395 | articleBeforeUpdateHooks = []ArticleHook{} 396 | 397 | AddArticleHook(boil.AfterUpdateHook, articleAfterUpdateHook) 398 | if err = o.doAfterUpdateHooks(ctx, nil); err != nil { 399 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 400 | } 401 | if !reflect.DeepEqual(o, empty) { 402 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 403 | } 404 | articleAfterUpdateHooks = []ArticleHook{} 405 | 406 | AddArticleHook(boil.BeforeDeleteHook, articleBeforeDeleteHook) 407 | if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { 408 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 409 | } 410 | if !reflect.DeepEqual(o, empty) { 411 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 412 | } 413 | articleBeforeDeleteHooks = []ArticleHook{} 414 | 415 | AddArticleHook(boil.AfterDeleteHook, articleAfterDeleteHook) 416 | if err = o.doAfterDeleteHooks(ctx, nil); err != nil { 417 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 418 | } 419 | if !reflect.DeepEqual(o, empty) { 420 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 421 | } 422 | articleAfterDeleteHooks = []ArticleHook{} 423 | 424 | AddArticleHook(boil.BeforeUpsertHook, articleBeforeUpsertHook) 425 | if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { 426 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 427 | } 428 | if !reflect.DeepEqual(o, empty) { 429 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 430 | } 431 | articleBeforeUpsertHooks = []ArticleHook{} 432 | 433 | AddArticleHook(boil.AfterUpsertHook, articleAfterUpsertHook) 434 | if err = o.doAfterUpsertHooks(ctx, nil); err != nil { 435 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 436 | } 437 | if !reflect.DeepEqual(o, empty) { 438 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 439 | } 440 | articleAfterUpsertHooks = []ArticleHook{} 441 | } 442 | 443 | func testArticlesInsert(t *testing.T) { 444 | t.Parallel() 445 | 446 | seed := randomize.NewSeed() 447 | var err error 448 | o := &Article{} 449 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 450 | t.Errorf("Unable to randomize Article struct: %s", err) 451 | } 452 | 453 | ctx := context.Background() 454 | tx := MustTx(boil.BeginTx(ctx, nil)) 455 | defer func() { _ = tx.Rollback() }() 456 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 457 | t.Error(err) 458 | } 459 | 460 | count, err := Articles().Count(ctx, tx) 461 | if err != nil { 462 | t.Error(err) 463 | } 464 | 465 | if count != 1 { 466 | t.Error("want one record, got:", count) 467 | } 468 | } 469 | 470 | func testArticlesInsertWhitelist(t *testing.T) { 471 | t.Parallel() 472 | 473 | seed := randomize.NewSeed() 474 | var err error 475 | o := &Article{} 476 | if err = randomize.Struct(seed, o, articleDBTypes, true); err != nil { 477 | t.Errorf("Unable to randomize Article struct: %s", err) 478 | } 479 | 480 | ctx := context.Background() 481 | tx := MustTx(boil.BeginTx(ctx, nil)) 482 | defer func() { _ = tx.Rollback() }() 483 | if err = o.Insert(ctx, tx, boil.Whitelist(articleColumnsWithoutDefault...)); err != nil { 484 | t.Error(err) 485 | } 486 | 487 | count, err := Articles().Count(ctx, tx) 488 | if err != nil { 489 | t.Error(err) 490 | } 491 | 492 | if count != 1 { 493 | t.Error("want one record, got:", count) 494 | } 495 | } 496 | 497 | func testArticlesReload(t *testing.T) { 498 | t.Parallel() 499 | 500 | seed := randomize.NewSeed() 501 | var err error 502 | o := &Article{} 503 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 504 | t.Errorf("Unable to randomize Article struct: %s", err) 505 | } 506 | 507 | ctx := context.Background() 508 | tx := MustTx(boil.BeginTx(ctx, nil)) 509 | defer func() { _ = tx.Rollback() }() 510 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 511 | t.Error(err) 512 | } 513 | 514 | if err = o.Reload(ctx, tx); err != nil { 515 | t.Error(err) 516 | } 517 | } 518 | 519 | func testArticlesReloadAll(t *testing.T) { 520 | t.Parallel() 521 | 522 | seed := randomize.NewSeed() 523 | var err error 524 | o := &Article{} 525 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 526 | t.Errorf("Unable to randomize Article struct: %s", err) 527 | } 528 | 529 | ctx := context.Background() 530 | tx := MustTx(boil.BeginTx(ctx, nil)) 531 | defer func() { _ = tx.Rollback() }() 532 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 533 | t.Error(err) 534 | } 535 | 536 | slice := ArticleSlice{o} 537 | 538 | if err = slice.ReloadAll(ctx, tx); err != nil { 539 | t.Error(err) 540 | } 541 | } 542 | 543 | func testArticlesSelect(t *testing.T) { 544 | t.Parallel() 545 | 546 | seed := randomize.NewSeed() 547 | var err error 548 | o := &Article{} 549 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 550 | t.Errorf("Unable to randomize Article struct: %s", err) 551 | } 552 | 553 | ctx := context.Background() 554 | tx := MustTx(boil.BeginTx(ctx, nil)) 555 | defer func() { _ = tx.Rollback() }() 556 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 557 | t.Error(err) 558 | } 559 | 560 | slice, err := Articles().All(ctx, tx) 561 | if err != nil { 562 | t.Error(err) 563 | } 564 | 565 | if len(slice) != 1 { 566 | t.Error("want one record, got:", len(slice)) 567 | } 568 | } 569 | 570 | var ( 571 | articleDBTypes = map[string]string{`ID`: `int`, `Title`: `varchar`, `Content`: `mediumtext`, `UserID`: `int`, `ProblemID`: `int`, `PublicTime`: `int`, `UpdateTime`: `int`, `SortTime`: `int`, `CommentsNum`: `int`, `AllowComment`: `tinyint`, `IsNotice`: `tinyint`} 572 | _ = bytes.MinRead 573 | ) 574 | 575 | func testArticlesUpdate(t *testing.T) { 576 | t.Parallel() 577 | 578 | if 0 == len(articlePrimaryKeyColumns) { 579 | t.Skip("Skipping table with no primary key columns") 580 | } 581 | if len(articleAllColumns) == len(articlePrimaryKeyColumns) { 582 | t.Skip("Skipping table with only primary key columns") 583 | } 584 | 585 | seed := randomize.NewSeed() 586 | var err error 587 | o := &Article{} 588 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 589 | t.Errorf("Unable to randomize Article struct: %s", err) 590 | } 591 | 592 | ctx := context.Background() 593 | tx := MustTx(boil.BeginTx(ctx, nil)) 594 | defer func() { _ = tx.Rollback() }() 595 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 596 | t.Error(err) 597 | } 598 | 599 | count, err := Articles().Count(ctx, tx) 600 | if err != nil { 601 | t.Error(err) 602 | } 603 | 604 | if count != 1 { 605 | t.Error("want one record, got:", count) 606 | } 607 | 608 | if err = randomize.Struct(seed, o, articleDBTypes, true, articlePrimaryKeyColumns...); err != nil { 609 | t.Errorf("Unable to randomize Article struct: %s", err) 610 | } 611 | 612 | if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { 613 | t.Error(err) 614 | } else if rowsAff != 1 { 615 | t.Error("should only affect one row but affected", rowsAff) 616 | } 617 | } 618 | 619 | func testArticlesSliceUpdateAll(t *testing.T) { 620 | t.Parallel() 621 | 622 | if len(articleAllColumns) == len(articlePrimaryKeyColumns) { 623 | t.Skip("Skipping table with only primary key columns") 624 | } 625 | 626 | seed := randomize.NewSeed() 627 | var err error 628 | o := &Article{} 629 | if err = randomize.Struct(seed, o, articleDBTypes, true, articleColumnsWithDefault...); err != nil { 630 | t.Errorf("Unable to randomize Article struct: %s", err) 631 | } 632 | 633 | ctx := context.Background() 634 | tx := MustTx(boil.BeginTx(ctx, nil)) 635 | defer func() { _ = tx.Rollback() }() 636 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 637 | t.Error(err) 638 | } 639 | 640 | count, err := Articles().Count(ctx, tx) 641 | if err != nil { 642 | t.Error(err) 643 | } 644 | 645 | if count != 1 { 646 | t.Error("want one record, got:", count) 647 | } 648 | 649 | if err = randomize.Struct(seed, o, articleDBTypes, true, articlePrimaryKeyColumns...); err != nil { 650 | t.Errorf("Unable to randomize Article struct: %s", err) 651 | } 652 | 653 | // Remove Primary keys and unique columns from what we plan to update 654 | var fields []string 655 | if strmangle.StringSliceMatch(articleAllColumns, articlePrimaryKeyColumns) { 656 | fields = articleAllColumns 657 | } else { 658 | fields = strmangle.SetComplement( 659 | articleAllColumns, 660 | articlePrimaryKeyColumns, 661 | ) 662 | } 663 | 664 | value := reflect.Indirect(reflect.ValueOf(o)) 665 | typ := reflect.TypeOf(o).Elem() 666 | n := typ.NumField() 667 | 668 | updateMap := M{} 669 | for _, col := range fields { 670 | for i := 0; i < n; i++ { 671 | f := typ.Field(i) 672 | if f.Tag.Get("boil") == col { 673 | updateMap[col] = value.Field(i).Interface() 674 | } 675 | } 676 | } 677 | 678 | slice := ArticleSlice{o} 679 | if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { 680 | t.Error(err) 681 | } else if rowsAff != 1 { 682 | t.Error("wanted one record updated but got", rowsAff) 683 | } 684 | } 685 | 686 | func testArticlesUpsert(t *testing.T) { 687 | t.Parallel() 688 | 689 | if len(articleAllColumns) == len(articlePrimaryKeyColumns) { 690 | t.Skip("Skipping table with only primary key columns") 691 | } 692 | if len(mySQLArticleUniqueColumns) == 0 { 693 | t.Skip("Skipping table with no unique columns to conflict on") 694 | } 695 | 696 | seed := randomize.NewSeed() 697 | var err error 698 | // Attempt the INSERT side of an UPSERT 699 | o := Article{} 700 | if err = randomize.Struct(seed, &o, articleDBTypes, false); err != nil { 701 | t.Errorf("Unable to randomize Article struct: %s", err) 702 | } 703 | 704 | ctx := context.Background() 705 | tx := MustTx(boil.BeginTx(ctx, nil)) 706 | defer func() { _ = tx.Rollback() }() 707 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 708 | t.Errorf("Unable to upsert Article: %s", err) 709 | } 710 | 711 | count, err := Articles().Count(ctx, tx) 712 | if err != nil { 713 | t.Error(err) 714 | } 715 | if count != 1 { 716 | t.Error("want one record, got:", count) 717 | } 718 | 719 | // Attempt the UPDATE side of an UPSERT 720 | if err = randomize.Struct(seed, &o, articleDBTypes, false, articlePrimaryKeyColumns...); err != nil { 721 | t.Errorf("Unable to randomize Article struct: %s", err) 722 | } 723 | 724 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 725 | t.Errorf("Unable to upsert Article: %s", err) 726 | } 727 | 728 | count, err = Articles().Count(ctx, tx) 729 | if err != nil { 730 | t.Error(err) 731 | } 732 | if count != 1 { 733 | t.Error("want one record, got:", count) 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /models/contest_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 3.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/volatiletech/sqlboiler/boil" 13 | "github.com/volatiletech/sqlboiler/queries" 14 | "github.com/volatiletech/sqlboiler/randomize" 15 | "github.com/volatiletech/sqlboiler/strmangle" 16 | ) 17 | 18 | var ( 19 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 20 | // so force a package dependency in case they don't. 21 | _ = queries.Equal 22 | ) 23 | 24 | func testContests(t *testing.T) { 25 | t.Parallel() 26 | 27 | query := Contests() 28 | 29 | if query.Query == nil { 30 | t.Error("expected a query, got nothing") 31 | } 32 | } 33 | 34 | func testContestsDelete(t *testing.T) { 35 | t.Parallel() 36 | 37 | seed := randomize.NewSeed() 38 | var err error 39 | o := &Contest{} 40 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 41 | t.Errorf("Unable to randomize Contest struct: %s", err) 42 | } 43 | 44 | ctx := context.Background() 45 | tx := MustTx(boil.BeginTx(ctx, nil)) 46 | defer func() { _ = tx.Rollback() }() 47 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 48 | t.Error(err) 49 | } 50 | 51 | if rowsAff, err := o.Delete(ctx, tx); err != nil { 52 | t.Error(err) 53 | } else if rowsAff != 1 { 54 | t.Error("should only have deleted one row, but affected:", rowsAff) 55 | } 56 | 57 | count, err := Contests().Count(ctx, tx) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if count != 0 { 63 | t.Error("want zero records, got:", count) 64 | } 65 | } 66 | 67 | func testContestsQueryDeleteAll(t *testing.T) { 68 | t.Parallel() 69 | 70 | seed := randomize.NewSeed() 71 | var err error 72 | o := &Contest{} 73 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 74 | t.Errorf("Unable to randomize Contest struct: %s", err) 75 | } 76 | 77 | ctx := context.Background() 78 | tx := MustTx(boil.BeginTx(ctx, nil)) 79 | defer func() { _ = tx.Rollback() }() 80 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if rowsAff, err := Contests().DeleteAll(ctx, tx); err != nil { 85 | t.Error(err) 86 | } else if rowsAff != 1 { 87 | t.Error("should only have deleted one row, but affected:", rowsAff) 88 | } 89 | 90 | count, err := Contests().Count(ctx, tx) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | if count != 0 { 96 | t.Error("want zero records, got:", count) 97 | } 98 | } 99 | 100 | func testContestsSliceDeleteAll(t *testing.T) { 101 | t.Parallel() 102 | 103 | seed := randomize.NewSeed() 104 | var err error 105 | o := &Contest{} 106 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 107 | t.Errorf("Unable to randomize Contest struct: %s", err) 108 | } 109 | 110 | ctx := context.Background() 111 | tx := MustTx(boil.BeginTx(ctx, nil)) 112 | defer func() { _ = tx.Rollback() }() 113 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 114 | t.Error(err) 115 | } 116 | 117 | slice := ContestSlice{o} 118 | 119 | if rowsAff, err := slice.DeleteAll(ctx, tx); err != nil { 120 | t.Error(err) 121 | } else if rowsAff != 1 { 122 | t.Error("should only have deleted one row, but affected:", rowsAff) 123 | } 124 | 125 | count, err := Contests().Count(ctx, tx) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | 130 | if count != 0 { 131 | t.Error("want zero records, got:", count) 132 | } 133 | } 134 | 135 | func testContestsExists(t *testing.T) { 136 | t.Parallel() 137 | 138 | seed := randomize.NewSeed() 139 | var err error 140 | o := &Contest{} 141 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 142 | t.Errorf("Unable to randomize Contest struct: %s", err) 143 | } 144 | 145 | ctx := context.Background() 146 | tx := MustTx(boil.BeginTx(ctx, nil)) 147 | defer func() { _ = tx.Rollback() }() 148 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 149 | t.Error(err) 150 | } 151 | 152 | e, err := ContestExists(ctx, tx, o.ID) 153 | if err != nil { 154 | t.Errorf("Unable to check if Contest exists: %s", err) 155 | } 156 | if !e { 157 | t.Errorf("Expected ContestExists to return true, but got false.") 158 | } 159 | } 160 | 161 | func testContestsFind(t *testing.T) { 162 | t.Parallel() 163 | 164 | seed := randomize.NewSeed() 165 | var err error 166 | o := &Contest{} 167 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 168 | t.Errorf("Unable to randomize Contest struct: %s", err) 169 | } 170 | 171 | ctx := context.Background() 172 | tx := MustTx(boil.BeginTx(ctx, nil)) 173 | defer func() { _ = tx.Rollback() }() 174 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 175 | t.Error(err) 176 | } 177 | 178 | contestFound, err := FindContest(ctx, tx, o.ID) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | 183 | if contestFound == nil { 184 | t.Error("want a record, got nil") 185 | } 186 | } 187 | 188 | func testContestsBind(t *testing.T) { 189 | t.Parallel() 190 | 191 | seed := randomize.NewSeed() 192 | var err error 193 | o := &Contest{} 194 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 195 | t.Errorf("Unable to randomize Contest struct: %s", err) 196 | } 197 | 198 | ctx := context.Background() 199 | tx := MustTx(boil.BeginTx(ctx, nil)) 200 | defer func() { _ = tx.Rollback() }() 201 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 202 | t.Error(err) 203 | } 204 | 205 | if err = Contests().Bind(ctx, tx, o); err != nil { 206 | t.Error(err) 207 | } 208 | } 209 | 210 | func testContestsOne(t *testing.T) { 211 | t.Parallel() 212 | 213 | seed := randomize.NewSeed() 214 | var err error 215 | o := &Contest{} 216 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 217 | t.Errorf("Unable to randomize Contest struct: %s", err) 218 | } 219 | 220 | ctx := context.Background() 221 | tx := MustTx(boil.BeginTx(ctx, nil)) 222 | defer func() { _ = tx.Rollback() }() 223 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 224 | t.Error(err) 225 | } 226 | 227 | if x, err := Contests().One(ctx, tx); err != nil { 228 | t.Error(err) 229 | } else if x == nil { 230 | t.Error("expected to get a non nil record") 231 | } 232 | } 233 | 234 | func testContestsAll(t *testing.T) { 235 | t.Parallel() 236 | 237 | seed := randomize.NewSeed() 238 | var err error 239 | contestOne := &Contest{} 240 | contestTwo := &Contest{} 241 | if err = randomize.Struct(seed, contestOne, contestDBTypes, false, contestColumnsWithDefault...); err != nil { 242 | t.Errorf("Unable to randomize Contest struct: %s", err) 243 | } 244 | if err = randomize.Struct(seed, contestTwo, contestDBTypes, false, contestColumnsWithDefault...); err != nil { 245 | t.Errorf("Unable to randomize Contest struct: %s", err) 246 | } 247 | 248 | ctx := context.Background() 249 | tx := MustTx(boil.BeginTx(ctx, nil)) 250 | defer func() { _ = tx.Rollback() }() 251 | if err = contestOne.Insert(ctx, tx, boil.Infer()); err != nil { 252 | t.Error(err) 253 | } 254 | if err = contestTwo.Insert(ctx, tx, boil.Infer()); err != nil { 255 | t.Error(err) 256 | } 257 | 258 | slice, err := Contests().All(ctx, tx) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | 263 | if len(slice) != 2 { 264 | t.Error("want 2 records, got:", len(slice)) 265 | } 266 | } 267 | 268 | func testContestsCount(t *testing.T) { 269 | t.Parallel() 270 | 271 | var err error 272 | seed := randomize.NewSeed() 273 | contestOne := &Contest{} 274 | contestTwo := &Contest{} 275 | if err = randomize.Struct(seed, contestOne, contestDBTypes, false, contestColumnsWithDefault...); err != nil { 276 | t.Errorf("Unable to randomize Contest struct: %s", err) 277 | } 278 | if err = randomize.Struct(seed, contestTwo, contestDBTypes, false, contestColumnsWithDefault...); err != nil { 279 | t.Errorf("Unable to randomize Contest struct: %s", err) 280 | } 281 | 282 | ctx := context.Background() 283 | tx := MustTx(boil.BeginTx(ctx, nil)) 284 | defer func() { _ = tx.Rollback() }() 285 | if err = contestOne.Insert(ctx, tx, boil.Infer()); err != nil { 286 | t.Error(err) 287 | } 288 | if err = contestTwo.Insert(ctx, tx, boil.Infer()); err != nil { 289 | t.Error(err) 290 | } 291 | 292 | count, err := Contests().Count(ctx, tx) 293 | if err != nil { 294 | t.Error(err) 295 | } 296 | 297 | if count != 2 { 298 | t.Error("want 2 records, got:", count) 299 | } 300 | } 301 | 302 | func contestBeforeInsertHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 303 | *o = Contest{} 304 | return nil 305 | } 306 | 307 | func contestAfterInsertHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 308 | *o = Contest{} 309 | return nil 310 | } 311 | 312 | func contestAfterSelectHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 313 | *o = Contest{} 314 | return nil 315 | } 316 | 317 | func contestBeforeUpdateHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 318 | *o = Contest{} 319 | return nil 320 | } 321 | 322 | func contestAfterUpdateHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 323 | *o = Contest{} 324 | return nil 325 | } 326 | 327 | func contestBeforeDeleteHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 328 | *o = Contest{} 329 | return nil 330 | } 331 | 332 | func contestAfterDeleteHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 333 | *o = Contest{} 334 | return nil 335 | } 336 | 337 | func contestBeforeUpsertHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 338 | *o = Contest{} 339 | return nil 340 | } 341 | 342 | func contestAfterUpsertHook(ctx context.Context, e boil.ContextExecutor, o *Contest) error { 343 | *o = Contest{} 344 | return nil 345 | } 346 | 347 | func testContestsHooks(t *testing.T) { 348 | t.Parallel() 349 | 350 | var err error 351 | 352 | ctx := context.Background() 353 | empty := &Contest{} 354 | o := &Contest{} 355 | 356 | seed := randomize.NewSeed() 357 | if err = randomize.Struct(seed, o, contestDBTypes, false); err != nil { 358 | t.Errorf("Unable to randomize Contest object: %s", err) 359 | } 360 | 361 | AddContestHook(boil.BeforeInsertHook, contestBeforeInsertHook) 362 | if err = o.doBeforeInsertHooks(ctx, nil); err != nil { 363 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 364 | } 365 | if !reflect.DeepEqual(o, empty) { 366 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 367 | } 368 | contestBeforeInsertHooks = []ContestHook{} 369 | 370 | AddContestHook(boil.AfterInsertHook, contestAfterInsertHook) 371 | if err = o.doAfterInsertHooks(ctx, nil); err != nil { 372 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 373 | } 374 | if !reflect.DeepEqual(o, empty) { 375 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 376 | } 377 | contestAfterInsertHooks = []ContestHook{} 378 | 379 | AddContestHook(boil.AfterSelectHook, contestAfterSelectHook) 380 | if err = o.doAfterSelectHooks(ctx, nil); err != nil { 381 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 382 | } 383 | if !reflect.DeepEqual(o, empty) { 384 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 385 | } 386 | contestAfterSelectHooks = []ContestHook{} 387 | 388 | AddContestHook(boil.BeforeUpdateHook, contestBeforeUpdateHook) 389 | if err = o.doBeforeUpdateHooks(ctx, nil); err != nil { 390 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 391 | } 392 | if !reflect.DeepEqual(o, empty) { 393 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 394 | } 395 | contestBeforeUpdateHooks = []ContestHook{} 396 | 397 | AddContestHook(boil.AfterUpdateHook, contestAfterUpdateHook) 398 | if err = o.doAfterUpdateHooks(ctx, nil); err != nil { 399 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 400 | } 401 | if !reflect.DeepEqual(o, empty) { 402 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 403 | } 404 | contestAfterUpdateHooks = []ContestHook{} 405 | 406 | AddContestHook(boil.BeforeDeleteHook, contestBeforeDeleteHook) 407 | if err = o.doBeforeDeleteHooks(ctx, nil); err != nil { 408 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 409 | } 410 | if !reflect.DeepEqual(o, empty) { 411 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 412 | } 413 | contestBeforeDeleteHooks = []ContestHook{} 414 | 415 | AddContestHook(boil.AfterDeleteHook, contestAfterDeleteHook) 416 | if err = o.doAfterDeleteHooks(ctx, nil); err != nil { 417 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 418 | } 419 | if !reflect.DeepEqual(o, empty) { 420 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 421 | } 422 | contestAfterDeleteHooks = []ContestHook{} 423 | 424 | AddContestHook(boil.BeforeUpsertHook, contestBeforeUpsertHook) 425 | if err = o.doBeforeUpsertHooks(ctx, nil); err != nil { 426 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 427 | } 428 | if !reflect.DeepEqual(o, empty) { 429 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 430 | } 431 | contestBeforeUpsertHooks = []ContestHook{} 432 | 433 | AddContestHook(boil.AfterUpsertHook, contestAfterUpsertHook) 434 | if err = o.doAfterUpsertHooks(ctx, nil); err != nil { 435 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 436 | } 437 | if !reflect.DeepEqual(o, empty) { 438 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 439 | } 440 | contestAfterUpsertHooks = []ContestHook{} 441 | } 442 | 443 | func testContestsInsert(t *testing.T) { 444 | t.Parallel() 445 | 446 | seed := randomize.NewSeed() 447 | var err error 448 | o := &Contest{} 449 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 450 | t.Errorf("Unable to randomize Contest struct: %s", err) 451 | } 452 | 453 | ctx := context.Background() 454 | tx := MustTx(boil.BeginTx(ctx, nil)) 455 | defer func() { _ = tx.Rollback() }() 456 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 457 | t.Error(err) 458 | } 459 | 460 | count, err := Contests().Count(ctx, tx) 461 | if err != nil { 462 | t.Error(err) 463 | } 464 | 465 | if count != 1 { 466 | t.Error("want one record, got:", count) 467 | } 468 | } 469 | 470 | func testContestsInsertWhitelist(t *testing.T) { 471 | t.Parallel() 472 | 473 | seed := randomize.NewSeed() 474 | var err error 475 | o := &Contest{} 476 | if err = randomize.Struct(seed, o, contestDBTypes, true); err != nil { 477 | t.Errorf("Unable to randomize Contest struct: %s", err) 478 | } 479 | 480 | ctx := context.Background() 481 | tx := MustTx(boil.BeginTx(ctx, nil)) 482 | defer func() { _ = tx.Rollback() }() 483 | if err = o.Insert(ctx, tx, boil.Whitelist(contestColumnsWithoutDefault...)); err != nil { 484 | t.Error(err) 485 | } 486 | 487 | count, err := Contests().Count(ctx, tx) 488 | if err != nil { 489 | t.Error(err) 490 | } 491 | 492 | if count != 1 { 493 | t.Error("want one record, got:", count) 494 | } 495 | } 496 | 497 | func testContestsReload(t *testing.T) { 498 | t.Parallel() 499 | 500 | seed := randomize.NewSeed() 501 | var err error 502 | o := &Contest{} 503 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 504 | t.Errorf("Unable to randomize Contest struct: %s", err) 505 | } 506 | 507 | ctx := context.Background() 508 | tx := MustTx(boil.BeginTx(ctx, nil)) 509 | defer func() { _ = tx.Rollback() }() 510 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 511 | t.Error(err) 512 | } 513 | 514 | if err = o.Reload(ctx, tx); err != nil { 515 | t.Error(err) 516 | } 517 | } 518 | 519 | func testContestsReloadAll(t *testing.T) { 520 | t.Parallel() 521 | 522 | seed := randomize.NewSeed() 523 | var err error 524 | o := &Contest{} 525 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 526 | t.Errorf("Unable to randomize Contest struct: %s", err) 527 | } 528 | 529 | ctx := context.Background() 530 | tx := MustTx(boil.BeginTx(ctx, nil)) 531 | defer func() { _ = tx.Rollback() }() 532 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 533 | t.Error(err) 534 | } 535 | 536 | slice := ContestSlice{o} 537 | 538 | if err = slice.ReloadAll(ctx, tx); err != nil { 539 | t.Error(err) 540 | } 541 | } 542 | 543 | func testContestsSelect(t *testing.T) { 544 | t.Parallel() 545 | 546 | seed := randomize.NewSeed() 547 | var err error 548 | o := &Contest{} 549 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 550 | t.Errorf("Unable to randomize Contest struct: %s", err) 551 | } 552 | 553 | ctx := context.Background() 554 | tx := MustTx(boil.BeginTx(ctx, nil)) 555 | defer func() { _ = tx.Rollback() }() 556 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 557 | t.Error(err) 558 | } 559 | 560 | slice, err := Contests().All(ctx, tx) 561 | if err != nil { 562 | t.Error(err) 563 | } 564 | 565 | if len(slice) != 1 { 566 | t.Error("want one record, got:", len(slice)) 567 | } 568 | } 569 | 570 | var ( 571 | contestDBTypes = map[string]string{`ID`: `int`, `Title`: `varchar`, `Subtitle`: `text`, `StartTime`: `int`, `EndTime`: `int`, `HolderID`: `int`, `Type`: `enum('noi','ioi','acm')`, `Information`: `text`, `Problems`: `text`, `Admins`: `text`, `RanklistID`: `int`, `IsPublic`: `tinyint`, `HideStatistics`: `tinyint`} 572 | _ = bytes.MinRead 573 | ) 574 | 575 | func testContestsUpdate(t *testing.T) { 576 | t.Parallel() 577 | 578 | if 0 == len(contestPrimaryKeyColumns) { 579 | t.Skip("Skipping table with no primary key columns") 580 | } 581 | if len(contestAllColumns) == len(contestPrimaryKeyColumns) { 582 | t.Skip("Skipping table with only primary key columns") 583 | } 584 | 585 | seed := randomize.NewSeed() 586 | var err error 587 | o := &Contest{} 588 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 589 | t.Errorf("Unable to randomize Contest struct: %s", err) 590 | } 591 | 592 | ctx := context.Background() 593 | tx := MustTx(boil.BeginTx(ctx, nil)) 594 | defer func() { _ = tx.Rollback() }() 595 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 596 | t.Error(err) 597 | } 598 | 599 | count, err := Contests().Count(ctx, tx) 600 | if err != nil { 601 | t.Error(err) 602 | } 603 | 604 | if count != 1 { 605 | t.Error("want one record, got:", count) 606 | } 607 | 608 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestPrimaryKeyColumns...); err != nil { 609 | t.Errorf("Unable to randomize Contest struct: %s", err) 610 | } 611 | 612 | if rowsAff, err := o.Update(ctx, tx, boil.Infer()); err != nil { 613 | t.Error(err) 614 | } else if rowsAff != 1 { 615 | t.Error("should only affect one row but affected", rowsAff) 616 | } 617 | } 618 | 619 | func testContestsSliceUpdateAll(t *testing.T) { 620 | t.Parallel() 621 | 622 | if len(contestAllColumns) == len(contestPrimaryKeyColumns) { 623 | t.Skip("Skipping table with only primary key columns") 624 | } 625 | 626 | seed := randomize.NewSeed() 627 | var err error 628 | o := &Contest{} 629 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestColumnsWithDefault...); err != nil { 630 | t.Errorf("Unable to randomize Contest struct: %s", err) 631 | } 632 | 633 | ctx := context.Background() 634 | tx := MustTx(boil.BeginTx(ctx, nil)) 635 | defer func() { _ = tx.Rollback() }() 636 | if err = o.Insert(ctx, tx, boil.Infer()); err != nil { 637 | t.Error(err) 638 | } 639 | 640 | count, err := Contests().Count(ctx, tx) 641 | if err != nil { 642 | t.Error(err) 643 | } 644 | 645 | if count != 1 { 646 | t.Error("want one record, got:", count) 647 | } 648 | 649 | if err = randomize.Struct(seed, o, contestDBTypes, true, contestPrimaryKeyColumns...); err != nil { 650 | t.Errorf("Unable to randomize Contest struct: %s", err) 651 | } 652 | 653 | // Remove Primary keys and unique columns from what we plan to update 654 | var fields []string 655 | if strmangle.StringSliceMatch(contestAllColumns, contestPrimaryKeyColumns) { 656 | fields = contestAllColumns 657 | } else { 658 | fields = strmangle.SetComplement( 659 | contestAllColumns, 660 | contestPrimaryKeyColumns, 661 | ) 662 | } 663 | 664 | value := reflect.Indirect(reflect.ValueOf(o)) 665 | typ := reflect.TypeOf(o).Elem() 666 | n := typ.NumField() 667 | 668 | updateMap := M{} 669 | for _, col := range fields { 670 | for i := 0; i < n; i++ { 671 | f := typ.Field(i) 672 | if f.Tag.Get("boil") == col { 673 | updateMap[col] = value.Field(i).Interface() 674 | } 675 | } 676 | } 677 | 678 | slice := ContestSlice{o} 679 | if rowsAff, err := slice.UpdateAll(ctx, tx, updateMap); err != nil { 680 | t.Error(err) 681 | } else if rowsAff != 1 { 682 | t.Error("wanted one record updated but got", rowsAff) 683 | } 684 | } 685 | 686 | func testContestsUpsert(t *testing.T) { 687 | t.Parallel() 688 | 689 | if len(contestAllColumns) == len(contestPrimaryKeyColumns) { 690 | t.Skip("Skipping table with only primary key columns") 691 | } 692 | if len(mySQLContestUniqueColumns) == 0 { 693 | t.Skip("Skipping table with no unique columns to conflict on") 694 | } 695 | 696 | seed := randomize.NewSeed() 697 | var err error 698 | // Attempt the INSERT side of an UPSERT 699 | o := Contest{} 700 | if err = randomize.Struct(seed, &o, contestDBTypes, false); err != nil { 701 | t.Errorf("Unable to randomize Contest struct: %s", err) 702 | } 703 | 704 | ctx := context.Background() 705 | tx := MustTx(boil.BeginTx(ctx, nil)) 706 | defer func() { _ = tx.Rollback() }() 707 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 708 | t.Errorf("Unable to upsert Contest: %s", err) 709 | } 710 | 711 | count, err := Contests().Count(ctx, tx) 712 | if err != nil { 713 | t.Error(err) 714 | } 715 | if count != 1 { 716 | t.Error("want one record, got:", count) 717 | } 718 | 719 | // Attempt the UPDATE side of an UPSERT 720 | if err = randomize.Struct(seed, &o, contestDBTypes, false, contestPrimaryKeyColumns...); err != nil { 721 | t.Errorf("Unable to randomize Contest struct: %s", err) 722 | } 723 | 724 | if err = o.Upsert(ctx, tx, boil.Infer(), boil.Infer()); err != nil { 725 | t.Errorf("Unable to upsert Contest: %s", err) 726 | } 727 | 728 | count, err = Contests().Count(ctx, tx) 729 | if err != nil { 730 | t.Error(err) 731 | } 732 | if count != 1 { 733 | t.Error("want one record, got:", count) 734 | } 735 | } 736 | --------------------------------------------------------------------------------