refactor(parser/diff): split to smaller functions
This commit is contained in:
		
							parent
							
								
									c3ba14d321
								
							
						
					
					
						commit
						997e7dde20
					
				|  | @ -7,24 +7,28 @@ import "github.com/joint-online-judge/JOJ3/internal/stage" | ||||||
| 
 | 
 | ||||||
| var name = "diff" | var name = "diff" | ||||||
| 
 | 
 | ||||||
|  | type Output struct { | ||||||
|  | 	Score            int | ||||||
|  | 	Filename         string | ||||||
|  | 	AnswerPath       string | ||||||
|  | 	CompareSpace     bool | ||||||
|  | 	AlwaysHide       bool | ||||||
|  | 	ForceQuitOnDiff  bool | ||||||
|  | 	MaxDiffLength    int `default:"2048"` // just for reference
 | ||||||
|  | 	MaxDiffLines     int `default:"50"`   // just for reference
 | ||||||
|  | 	HideCommonPrefix bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Case struct { | ||||||
|  | 	Outputs []Output | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type Conf struct { | type Conf struct { | ||||||
| 	PassComment       string `default:"🥳Passed!"` | 	PassComment       string `default:"🥳Passed!"` | ||||||
| 	FailComment       string `default:"🧐Failed..."` | 	FailComment       string `default:"🧐Failed..."` | ||||||
| 	FailOnNotAccepted bool   `default:"true"` | 	FailOnNotAccepted bool   `default:"true"` | ||||||
| 	ForceQuitOnFailed bool   `default:"false"` | 	ForceQuitOnFailed bool   `default:"false"` | ||||||
| 	Cases             []struct { | 	Cases             []Case | ||||||
| 		Outputs []struct { |  | ||||||
| 			Score            int |  | ||||||
| 			Filename         string |  | ||||||
| 			AnswerPath       string |  | ||||||
| 			CompareSpace     bool |  | ||||||
| 			AlwaysHide       bool |  | ||||||
| 			ForceQuitOnDiff  bool |  | ||||||
| 			MaxDiffLength    int `default:"2048"` // just for reference
 |  | ||||||
| 			MaxDiffLines     int `default:"50"`   // just for reference
 |  | ||||||
| 			HideCommonPrefix bool |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Diff struct{} | type Diff struct{} | ||||||
|  |  | ||||||
|  | @ -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,124 +24,168 @@ 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] | ||||||
| 		score := 0 | 		parserResult, fq, err := d.processCase(caseConf, result, conf, i) | ||||||
| 		comment := "" | 		if err != nil { | ||||||
| 		if conf.FailOnNotAccepted && | 			return nil, true, err | ||||||
| 			result.Status != stage.StatusAccepted { | 		} | ||||||
| 			if conf.ForceQuitOnFailed { | 		res = append(res, parserResult) | ||||||
| 				forceQuit = true | 		if fq { | ||||||
| 			} | 			forceQuit = true | ||||||
| 			comment += conf.FailComment + "\n" |  | ||||||
| 			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 |  | ||||||
| 				} |  | ||||||
| 				answerStr := string(answer) |  | ||||||
| 				resultStr := result.Files[output.Filename] |  | ||||||
| 				isSame := stringsEqual( |  | ||||||
| 					answerStr, |  | ||||||
| 					resultStr, |  | ||||||
| 					output.CompareSpace, |  | ||||||
| 				) |  | ||||||
| 				slog.Debug( |  | ||||||
| 					"compare", |  | ||||||
| 					"filename", output.Filename, |  | ||||||
| 					"answerPath", output.AnswerPath, |  | ||||||
| 					"actualLength", len(resultStr), |  | ||||||
| 					"answerLength", len(answerStr), |  | ||||||
| 					"index", i, |  | ||||||
| 					"isSame", isSame, |  | ||||||
| 				) |  | ||||||
| 				// If no difference, assign score
 |  | ||||||
| 				if isSame { |  | ||||||
| 					score += output.Score |  | ||||||
| 					comment += conf.PassComment + "\n" |  | ||||||
| 				} else { |  | ||||||
| 					if output.ForceQuitOnDiff || conf.ForceQuitOnFailed { |  | ||||||
| 						forceQuit = true |  | ||||||
| 					} |  | ||||||
| 					comment += conf.FailComment + "\n" |  | ||||||
| 					comment += fmt.Sprintf("Difference found in `%s`\n", |  | ||||||
| 						output.Filename) |  | ||||||
| 					if !output.AlwaysHide { |  | ||||||
| 						if output.MaxDiffLength == 0 { // real default value
 |  | ||||||
| 							output.MaxDiffLength = 2048 |  | ||||||
| 						} |  | ||||||
| 						if output.MaxDiffLines == 0 { // real default value
 |  | ||||||
| 							output.MaxDiffLines = 50 |  | ||||||
| 						} |  | ||||||
| 						// Convert answer to string and split by lines
 |  | ||||||
| 						truncated := false |  | ||||||
| 						if len(answerStr) > output.MaxDiffLength { |  | ||||||
| 							answerStr = answerStr[:output.MaxDiffLength] |  | ||||||
| 							truncated = true |  | ||||||
| 						} |  | ||||||
| 						if len(resultStr) > output.MaxDiffLength { |  | ||||||
| 							resultStr = resultStr[:output.MaxDiffLength] |  | ||||||
| 							truncated = true |  | ||||||
| 						} |  | ||||||
| 						answerLines := strings.Split(answerStr, "\n") |  | ||||||
| 						resultLines := strings.Split(resultStr, "\n") |  | ||||||
| 						commonPrefixLineCount := 0 |  | ||||||
| 						if output.HideCommonPrefix { |  | ||||||
| 							n := 0 |  | ||||||
| 							for ; n < len(answerLines) && |  | ||||||
| 								n < len(resultLines) && |  | ||||||
| 								stringsEqual( |  | ||||||
| 									answerLines[n], |  | ||||||
| 									resultLines[n], |  | ||||||
| 									output.CompareSpace, |  | ||||||
| 								); n += 1 { |  | ||||||
| 							} |  | ||||||
| 							if n > 0 { |  | ||||||
| 								answerLines = answerLines[n-1:] |  | ||||||
| 								resultLines = resultLines[n-1:] |  | ||||||
| 								commonPrefixLineCount = n |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 						if len(answerLines) > output.MaxDiffLines { |  | ||||||
| 							answerLines = answerLines[:output.MaxDiffLines] |  | ||||||
| 							truncated = true |  | ||||||
| 						} |  | ||||||
| 						if len(resultLines) > output.MaxDiffLines { |  | ||||||
| 							resultLines = resultLines[:output.MaxDiffLines] |  | ||||||
| 							truncated = true |  | ||||||
| 						} |  | ||||||
| 						diffs := patienceDiff( |  | ||||||
| 							answerLines, |  | ||||||
| 							resultLines, |  | ||||||
| 							func(a, b string) bool { |  | ||||||
| 								return stringsEqual(a, b, output.CompareSpace) |  | ||||||
| 							}) |  | ||||||
| 						diffOutput := diffText(diffs) |  | ||||||
| 						diffOutput = strings.TrimSuffix(diffOutput, "\n  ") |  | ||||||
| 						if truncated { |  | ||||||
| 							diffOutput += "\n\n(truncated)" |  | ||||||
| 						} |  | ||||||
| 						if commonPrefixLineCount > 0 { |  | ||||||
| 							diffOutput = fmt.Sprintf( |  | ||||||
| 								"(%d line(s) of common prefix hidden)\n\n", |  | ||||||
| 								commonPrefixLineCount, |  | ||||||
| 							) + diffOutput |  | ||||||
| 						} |  | ||||||
| 						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 | 	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 | ||||||
|  | 	comment := "" | ||||||
|  | 	forceQuit := false | ||||||
|  | 
 | ||||||
|  | 	if conf.FailOnNotAccepted && result.Status != stage.StatusAccepted { | ||||||
|  | 		if conf.ForceQuitOnFailed { | ||||||
|  | 			forceQuit = true | ||||||
|  | 		} | ||||||
|  | 		comment += conf.FailComment + "\n" | ||||||
|  | 		comment += "Executor status not `Accepted`\n" | ||||||
|  | 	} else { | ||||||
|  | 		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) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, "", true, err | ||||||
|  | 	} | ||||||
|  | 	answerStr := string(answer) | ||||||
|  | 	resultStr := result.Files[output.Filename] | ||||||
|  | 
 | ||||||
|  | 	isSame := stringsEqual( | ||||||
|  | 		answerStr, | ||||||
|  | 		resultStr, | ||||||
|  | 		output.CompareSpace, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	slog.Debug( | ||||||
|  | 		"compare", | ||||||
|  | 		"filename", output.Filename, | ||||||
|  | 		"answerPath", output.AnswerPath, | ||||||
|  | 		"actualLength", len(resultStr), | ||||||
|  | 		"answerLength", len(answerStr), | ||||||
|  | 		"index", index, | ||||||
|  | 		"isSame", isSame, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if isSame { | ||||||
|  | 		return output.Score, conf.PassComment + "\n", false, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// They are different.
 | ||||||
|  | 	forceQuit := false | ||||||
|  | 	if output.ForceQuitOnDiff || conf.ForceQuitOnFailed { | ||||||
|  | 		forceQuit = true | ||||||
|  | 	} | ||||||
|  | 	comment := conf.FailComment + "\n" | ||||||
|  | 	comment += fmt.Sprintf("Difference found in `%s`\n", output.Filename) | ||||||
|  | 
 | ||||||
|  | 	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
 | ||||||
|  | 		output.MaxDiffLength = 2048 | ||||||
|  | 	} | ||||||
|  | 	if output.MaxDiffLines == 0 { // real default value
 | ||||||
|  | 		output.MaxDiffLines = 50 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	truncated := false | ||||||
|  | 	if len(answerStr) > output.MaxDiffLength { | ||||||
|  | 		answerStr = answerStr[:output.MaxDiffLength] | ||||||
|  | 		truncated = true | ||||||
|  | 	} | ||||||
|  | 	if len(resultStr) > output.MaxDiffLength { | ||||||
|  | 		resultStr = resultStr[:output.MaxDiffLength] | ||||||
|  | 		truncated = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	answerLines := strings.Split(answerStr, "\n") | ||||||
|  | 	resultLines := strings.Split(resultStr, "\n") | ||||||
|  | 	commonPrefixLineCount := 0 | ||||||
|  | 
 | ||||||
|  | 	if output.HideCommonPrefix { | ||||||
|  | 		n := 0 | ||||||
|  | 		for ; n < len(answerLines) && | ||||||
|  | 			n < len(resultLines) && | ||||||
|  | 			stringsEqual( | ||||||
|  | 				answerLines[n], | ||||||
|  | 				resultLines[n], | ||||||
|  | 				output.CompareSpace, | ||||||
|  | 			); n += 1 { | ||||||
|  | 		} | ||||||
|  | 		if n > 0 { | ||||||
|  | 			answerLines = answerLines[n-1:] | ||||||
|  | 			resultLines = resultLines[n-1:] | ||||||
|  | 			commonPrefixLineCount = n | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(answerLines) > output.MaxDiffLines { | ||||||
|  | 		answerLines = answerLines[:output.MaxDiffLines] | ||||||
|  | 		truncated = true | ||||||
|  | 	} | ||||||
|  | 	if len(resultLines) > output.MaxDiffLines { | ||||||
|  | 		resultLines = resultLines[:output.MaxDiffLines] | ||||||
|  | 		truncated = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	diffs := patienceDiff( | ||||||
|  | 		answerLines, | ||||||
|  | 		resultLines, | ||||||
|  | 		func(a, b string) bool { | ||||||
|  | 			return stringsEqual(a, b, output.CompareSpace) | ||||||
|  | 		}) | ||||||
|  | 	diffOutput := diffText(diffs) | ||||||
|  | 	diffOutput = strings.TrimSuffix(diffOutput, "\n  ") | ||||||
|  | 
 | ||||||
|  | 	if truncated { | ||||||
|  | 		diffOutput += "\n\n(truncated)" | ||||||
|  | 	} | ||||||
|  | 	if commonPrefixLineCount > 0 { | ||||||
|  | 		diffOutput = fmt.Sprintf( | ||||||
|  | 			"(%d line(s) of common prefix hidden)\n\n", | ||||||
|  | 			commonPrefixLineCount, | ||||||
|  | 		) + diffOutput | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return fmt.Sprintf( | ||||||
|  | 		"```diff\n%s\n```\n", | ||||||
|  | 		diffOutput, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user