126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package diff
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/criyle/go-judge/envexec"
|
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
|
)
|
|
|
|
// operation represents the type of edit operation.
|
|
type operation uint
|
|
|
|
const (
|
|
INSERT operation = iota + 1
|
|
DELETE
|
|
MOVE
|
|
)
|
|
|
|
type Conf struct {
|
|
PassComment string `default:"🥳Passed!\n"`
|
|
FailComment string `default:"🧐Failed...\n"`
|
|
FailOnNotAccepted bool `default:"true"`
|
|
ForceQuitOnFailed bool `default:"false"`
|
|
Cases []struct {
|
|
Outputs []struct {
|
|
Score int
|
|
FileName string
|
|
AnswerPath string
|
|
CompareSpace bool
|
|
AlwaysHide bool
|
|
ForceQuitOnDiff bool
|
|
MaxDiffLength int `default:"2048"` // just for reference
|
|
}
|
|
}
|
|
}
|
|
|
|
type Diff struct{}
|
|
|
|
func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
|
[]stage.ParserResult, bool, error,
|
|
) {
|
|
conf, err := stage.DecodeConf[Conf](confAny)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
if len(conf.Cases) != len(results) {
|
|
return nil, true, fmt.Errorf("cases number not match")
|
|
}
|
|
|
|
var res []stage.ParserResult
|
|
forceQuit := false
|
|
for i, caseConf := range conf.Cases {
|
|
result := results[i]
|
|
score := 0
|
|
comment := ""
|
|
if conf.FailOnNotAccepted &&
|
|
result.Status != stage.Status(envexec.StatusAccepted) {
|
|
if conf.ForceQuitOnFailed {
|
|
forceQuit = true
|
|
}
|
|
comment += conf.FailComment
|
|
comment += "Executor status not `Accepted`\n"
|
|
} else {
|
|
for _, output := range caseConf.Outputs {
|
|
answer, err := os.ReadFile(output.AnswerPath)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
slog.Debug("compare", "filename", output.FileName,
|
|
"answer path", output.AnswerPath,
|
|
"actual length", len(result.Files[output.FileName]),
|
|
"answer length", len(string(answer)))
|
|
// If no difference, assign score
|
|
if compareStrings(string(answer), result.Files[output.FileName],
|
|
output.CompareSpace) {
|
|
score += output.Score
|
|
comment += conf.PassComment
|
|
} else {
|
|
if output.ForceQuitOnDiff || conf.ForceQuitOnFailed {
|
|
forceQuit = true
|
|
}
|
|
comment += conf.FailComment
|
|
comment += fmt.Sprintf("Difference found in `%s`\n",
|
|
output.FileName)
|
|
if !output.AlwaysHide {
|
|
// Convert answer to string and split by lines
|
|
stdoutLines := strings.Split(string(answer), "\n")
|
|
resultLines := strings.Split(
|
|
result.Files[output.FileName], "\n")
|
|
|
|
// Generate Myers diff
|
|
diffOps := myersDiff(stdoutLines, resultLines,
|
|
output.CompareSpace)
|
|
if output.MaxDiffLength == 0 { // real default value
|
|
output.MaxDiffLength = 2048
|
|
}
|
|
// Generate diff block with surrounding context
|
|
diffOutput := generateDiffWithContext(
|
|
stdoutLines,
|
|
resultLines,
|
|
diffOps,
|
|
output.MaxDiffLength,
|
|
)
|
|
diffOutput = strings.TrimSuffix(diffOutput, "\n \n")
|
|
comment += fmt.Sprintf(
|
|
"```diff\n%s\n```\n",
|
|
diffOutput,
|
|
)
|
|
} else {
|
|
comment += "(Content hidden.)\n"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
res = append(res, stage.ParserResult{
|
|
Score: score,
|
|
Comment: comment,
|
|
})
|
|
}
|
|
|
|
return res, forceQuit, nil
|
|
}
|