refactor(parser/diff): split to smaller functions
All checks were successful
submodules sync / sync (push) Successful in 49s
build / build (push) Successful in 2m53s
build / trigger-build-image (push) Successful in 12s

This commit is contained in:
张泊明518370910136 2025-07-16 21:05:28 -07:00
parent c3ba14d321
commit 997e7dde20
2 changed files with 178 additions and 130 deletions

View File

@ -7,13 +7,7 @@ import "github.com/joint-online-judge/JOJ3/internal/stage"
var name = "diff" var name = "diff"
type Conf struct { type Output struct {
PassComment string `default:"🥳Passed!"`
FailComment string `default:"🧐Failed..."`
FailOnNotAccepted bool `default:"true"`
ForceQuitOnFailed bool `default:"false"`
Cases []struct {
Outputs []struct {
Score int Score int
Filename string Filename string
AnswerPath string AnswerPath string
@ -23,8 +17,18 @@ type Conf struct {
MaxDiffLength int `default:"2048"` // just for reference MaxDiffLength int `default:"2048"` // just for reference
MaxDiffLines int `default:"50"` // just for reference MaxDiffLines int `default:"50"` // just for reference
HideCommonPrefix bool HideCommonPrefix bool
} }
}
type Case struct {
Outputs []Output
}
type Conf struct {
PassComment string `default:"🥳Passed!"`
FailComment string `default:"🧐Failed..."`
FailOnNotAccepted bool `default:"true"`
ForceQuitOnFailed bool `default:"false"`
Cases []Case
} }
type Diff struct{} type Diff struct{}

View File

@ -9,7 +9,7 @@ import (
"github.com/joint-online-judge/JOJ3/internal/stage" "github.com/joint-online-judge/JOJ3/internal/stage"
) )
func (*Diff) Run(results []stage.ExecutorResult, confAny any) ( func (d *Diff) Run(results []stage.ExecutorResult, confAny any) (
[]stage.ParserResult, bool, error, []stage.ParserResult, bool, error,
) { ) {
conf, err := stage.DecodeConf[Conf](confAny) conf, err := stage.DecodeConf[Conf](confAny)
@ -24,10 +24,26 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
forceQuit := false forceQuit := false
for i, caseConf := range conf.Cases { for i, caseConf := range conf.Cases {
result := results[i] result := results[i]
parserResult, fq, err := d.processCase(caseConf, result, conf, i)
if err != nil {
return nil, true, err
}
res = append(res, parserResult)
if fq {
forceQuit = true
}
}
return res, forceQuit, nil
}
// processCase handles a single test case.
func (d *Diff) processCase(caseConf Case, result stage.ExecutorResult, conf *Conf, index int) (stage.ParserResult, bool, error) {
score := 0 score := 0
comment := "" comment := ""
if conf.FailOnNotAccepted && forceQuit := false
result.Status != stage.StatusAccepted {
if conf.FailOnNotAccepted && result.Status != stage.StatusAccepted {
if conf.ForceQuitOnFailed { if conf.ForceQuitOnFailed {
forceQuit = true forceQuit = true
} }
@ -35,45 +51,80 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
comment += "Executor status not `Accepted`\n" comment += "Executor status not `Accepted`\n"
} else { } else {
for _, output := range caseConf.Outputs { for _, output := range caseConf.Outputs {
outputScore, outputComment, outputForceQuit, err := d.processOutput(output, result, conf, index)
if err != nil {
return stage.ParserResult{}, true, err
}
score += outputScore
comment += outputComment
if outputForceQuit {
forceQuit = true
}
}
}
return stage.ParserResult{
Score: score,
Comment: comment,
}, forceQuit, nil
}
// processOutput handles a single output comparison.
func (d *Diff) processOutput(output Output, result stage.ExecutorResult, conf *Conf, index int) (int, string, bool, error) {
answer, err := os.ReadFile(output.AnswerPath) answer, err := os.ReadFile(output.AnswerPath)
if err != nil { if err != nil {
return nil, true, err return 0, "", true, err
} }
answerStr := string(answer) answerStr := string(answer)
resultStr := result.Files[output.Filename] resultStr := result.Files[output.Filename]
isSame := stringsEqual( isSame := stringsEqual(
answerStr, answerStr,
resultStr, resultStr,
output.CompareSpace, output.CompareSpace,
) )
slog.Debug( slog.Debug(
"compare", "compare",
"filename", output.Filename, "filename", output.Filename,
"answerPath", output.AnswerPath, "answerPath", output.AnswerPath,
"actualLength", len(resultStr), "actualLength", len(resultStr),
"answerLength", len(answerStr), "answerLength", len(answerStr),
"index", i, "index", index,
"isSame", isSame, "isSame", isSame,
) )
// If no difference, assign score
if isSame { if isSame {
score += output.Score return output.Score, conf.PassComment + "\n", false, nil
comment += conf.PassComment + "\n" }
} else {
// They are different.
forceQuit := false
if output.ForceQuitOnDiff || conf.ForceQuitOnFailed { if output.ForceQuitOnDiff || conf.ForceQuitOnFailed {
forceQuit = true forceQuit = true
} }
comment += conf.FailComment + "\n" comment := conf.FailComment + "\n"
comment += fmt.Sprintf("Difference found in `%s`\n", comment += fmt.Sprintf("Difference found in `%s`\n", output.Filename)
output.Filename)
if !output.AlwaysHide { if !output.AlwaysHide {
diffComment := d.generateDiffComment(answerStr, resultStr, output)
comment += diffComment
} else {
comment += "(content hidden)\n"
}
return 0, comment, forceQuit, nil
}
// generateDiffComment generates a diff comment for the given strings.
func (d *Diff) generateDiffComment(answerStr, resultStr string, output Output) string {
if output.MaxDiffLength == 0 { // real default value if output.MaxDiffLength == 0 { // real default value
output.MaxDiffLength = 2048 output.MaxDiffLength = 2048
} }
if output.MaxDiffLines == 0 { // real default value if output.MaxDiffLines == 0 { // real default value
output.MaxDiffLines = 50 output.MaxDiffLines = 50
} }
// Convert answer to string and split by lines
truncated := false truncated := false
if len(answerStr) > output.MaxDiffLength { if len(answerStr) > output.MaxDiffLength {
answerStr = answerStr[:output.MaxDiffLength] answerStr = answerStr[:output.MaxDiffLength]
@ -83,9 +134,11 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
resultStr = resultStr[:output.MaxDiffLength] resultStr = resultStr[:output.MaxDiffLength]
truncated = true truncated = true
} }
answerLines := strings.Split(answerStr, "\n") answerLines := strings.Split(answerStr, "\n")
resultLines := strings.Split(resultStr, "\n") resultLines := strings.Split(resultStr, "\n")
commonPrefixLineCount := 0 commonPrefixLineCount := 0
if output.HideCommonPrefix { if output.HideCommonPrefix {
n := 0 n := 0
for ; n < len(answerLines) && for ; n < len(answerLines) &&
@ -102,6 +155,7 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
commonPrefixLineCount = n commonPrefixLineCount = n
} }
} }
if len(answerLines) > output.MaxDiffLines { if len(answerLines) > output.MaxDiffLines {
answerLines = answerLines[:output.MaxDiffLines] answerLines = answerLines[:output.MaxDiffLines]
truncated = true truncated = true
@ -110,6 +164,7 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
resultLines = resultLines[:output.MaxDiffLines] resultLines = resultLines[:output.MaxDiffLines]
truncated = true truncated = true
} }
diffs := patienceDiff( diffs := patienceDiff(
answerLines, answerLines,
resultLines, resultLines,
@ -118,6 +173,7 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
}) })
diffOutput := diffText(diffs) diffOutput := diffText(diffs)
diffOutput = strings.TrimSuffix(diffOutput, "\n ") diffOutput = strings.TrimSuffix(diffOutput, "\n ")
if truncated { if truncated {
diffOutput += "\n\n(truncated)" diffOutput += "\n\n(truncated)"
} }
@ -127,21 +183,9 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
commonPrefixLineCount, commonPrefixLineCount,
) + diffOutput ) + diffOutput
} }
comment += fmt.Sprintf(
return fmt.Sprintf(
"```diff\n%s\n```\n", "```diff\n%s\n```\n",
diffOutput, diffOutput,
) )
} else {
comment += "(content hidden)\n"
}
}
}
}
res = append(res, stage.ParserResult{
Score: score,
Comment: comment,
})
}
return res, forceQuit, nil
} }