feat: diff parser #33
8
.gitmodules
vendored
8
.gitmodules
vendored
|
@ -1,3 +1,11 @@
|
|||
[submodule "examples/diff/basic"]
|
||||
path = examples/diff/basic
|
||||
url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
|
||||
branch = diff/basic
|
||||
[submodule "examples/diff/complex"]
|
||||
path = examples/diff/complex
|
||||
url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
|
||||
branch = diff/complex
|
||||
[submodule "examples/cpplint/sillycode"]
|
||||
path = examples/cpplint/sillycode
|
||||
url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 2593e79505a93042d308c5fc355dba671dd4fdba
|
||||
Subproject commit 4e5fab93e5a0ce67c8f40fef1e8f4cab7018fc5d
|
|
@ -1 +1 @@
|
|||
Subproject commit 638e9f661092d39daaf6e1ffc8ba5998fc56c96a
|
||||
Subproject commit 1512cb5f20473a598d7504a08dacff3d6406b983
|
1
examples/diff/basic
Submodule
1
examples/diff/basic
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit af990327ab095c22a383448ad70d915f8d10490b
|
1
examples/diff/complex
Submodule
1
examples/diff/complex
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit ac7a2fc912fb51af156cd4babb7e72148ebe1c14
|
|
@ -3,14 +3,29 @@ package diff
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
|
||||
)
|
||||
|
||||
// operation represents the type of edit operation.
|
||||
type operation uint
|
||||
|
||||
const (
|
||||
INSERT operation = iota + 1
|
||||
DELETE
|
||||
MOVE
|
||||
)
|
||||
|
||||
type Conf struct {
|
||||
Cases []struct {
|
||||
Score int
|
||||
StdoutPath string
|
||||
Outputs []struct {
|
||||
Score int
|
||||
FileName string
|
||||
AnswerPath string
|
||||
IgnoreWhitespace bool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,25 +41,195 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
|||
if len(conf.Cases) != len(results) {
|
||||
return nil, true, fmt.Errorf("cases number not match")
|
||||
}
|
||||
|
||||
var res []stage.ParserResult
|
||||
for i, caseConf := range conf.Cases {
|
||||
result := results[i]
|
||||
score := 0
|
||||
stdout, err := os.ReadFile(caseConf.StdoutPath)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
// TODO: more compare strategies
|
||||
if string(stdout) == result.Files["stdout"] {
|
||||
score = caseConf.Score
|
||||
comment := ""
|
||||
for _, output := range caseConf.Outputs {
|
||||
answer, err := os.ReadFile(output.AnswerPath)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
|
||||
// If no difference, assign score
|
||||
if compareChars(string(answer), result.Files[output.FileName], output.IgnoreWhitespace) {
|
||||
score += output.Score
|
||||
} else {
|
||||
// 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)
|
||||
|
||||
// Generate diff block with surrounding context
|
||||
diffOutput := generateDiffWithContext(stdoutLines, resultLines, diffOps)
|
||||
comment += fmt.Sprintf(
|
||||
"difference found in %s:\n```diff\n%s```\n",
|
||||
output.FileName, diffOutput,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, stage.ParserResult{
|
||||
Score: score,
|
||||
Comment: fmt.Sprintf(
|
||||
"executor status: run time: %d ns, memory: %d bytes",
|
||||
result.RunTime, result.Memory,
|
||||
),
|
||||
Score: score,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
return res, false, nil
|
||||
}
|
||||
|
||||
// compareChars compares two strings character by character, optionally ignoring whitespace.
|
||||
func compareChars(stdout, result string, ignoreWhitespace bool) bool {
|
||||
if ignoreWhitespace {
|
||||
stdout = removeWhitespace(stdout)
|
||||
result = removeWhitespace(result)
|
||||
}
|
||||
return stdout == result
|
||||
}
|
||||
|
||||
// removeWhitespace removes all whitespace characters from the string.
|
||||
func removeWhitespace(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if !unicode.IsSpace(r) {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// myersDiff computes the Myers' diff between two slices of strings.
|
||||
// src: https://github.com/cj1128/myers-diff/blob/master/main.go
|
||||
func myersDiff(src, dst []string) []operation {
|
||||
n := len(src)
|
||||
m := len(dst)
|
||||
max := n + m
|
||||
var trace []map[int]int
|
||||
var x, y int
|
||||
|
||||
loop:
|
||||
for d := 0; d <= max; d++ {
|
||||
v := make(map[int]int, d+2)
|
||||
trace = append(trace, v)
|
||||
|
||||
if d == 0 {
|
||||
t := 0
|
||||
for len(src) > t && len(dst) > t && src[t] == dst[t] {
|
||||
t++
|
||||
}
|
||||
v[0] = t
|
||||
if t == len(src) && len(src) == len(dst) {
|
||||
break loop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
lastV := trace[d-1]
|
||||
|
||||
for k := -d; k <= d; k += 2 {
|
||||
if k == -d || (k != d && lastV[k-1] < lastV[k+1]) {
|
||||
x = lastV[k+1]
|
||||
} else {
|
||||
x = lastV[k-1] + 1
|
||||
}
|
||||
|
||||
y = x - k
|
||||
|
||||
for x < n && y < m && src[x] == dst[y] {
|
||||
x, y = x+1, y+1
|
||||
}
|
||||
|
||||
v[k] = x
|
||||
|
||||
if x == n && y == m {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var script []operation
|
||||
x = n
|
||||
y = m
|
||||
var k, prevK, prevX, prevY int
|
||||
|
||||
for d := len(trace) - 1; d > 0; d-- {
|
||||
k = x - y
|
||||
lastV := trace[d-1]
|
||||
|
||||
if k == -d || (k != d && lastV[k-1] < lastV[k+1]) {
|
||||
prevK = k + 1
|
||||
} else {
|
||||
prevK = k - 1
|
||||
}
|
||||
|
||||
prevX = lastV[prevK]
|
||||
prevY = prevX - prevK
|
||||
|
||||
for x > prevX && y > prevY {
|
||||
script = append(script, MOVE)
|
||||
x -= 1
|
||||
y -= 1
|
||||
}
|
||||
|
||||
if x == prevX {
|
||||
script = append(script, INSERT)
|
||||
} else {
|
||||
script = append(script, DELETE)
|
||||
}
|
||||
|
||||
x, y = prevX, prevY
|
||||
}
|
||||
|
||||
if trace[0][0] != 0 {
|
||||
for i := 0; i < trace[0][0]; i++ {
|
||||
script = append(script, MOVE)
|
||||
}
|
||||
}
|
||||
|
||||
return reverse(script)
|
||||
}
|
||||
|
||||
// reverse reverses a slice of operations.
|
||||
func reverse(s []operation) []operation {
|
||||
result := make([]operation, len(s))
|
||||
for i, v := range s {
|
||||
result[len(s)-1-i] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// generateDiffWithContext creates a diff block with surrounding context from stdout and result.
|
||||
func generateDiffWithContext(stdoutLines, resultLines []string, ops []operation) string {
|
||||
var diffBuilder strings.Builder
|
||||
|
||||
srcIndex, dstIndex := 0, 0
|
||||
|
||||
for _, op := range ops {
|
||||
switch op {
|
||||
case INSERT:
|
||||
if dstIndex < len(resultLines) {
|
||||
diffBuilder.WriteString(fmt.Sprintf("+ %s\n", resultLines[dstIndex]))
|
||||
dstIndex++
|
||||
}
|
||||
|
||||
case MOVE:
|
||||
if srcIndex < len(stdoutLines) {
|
||||
diffBuilder.WriteString(fmt.Sprintf(" %s\n", stdoutLines[srcIndex]))
|
||||
srcIndex++
|
||||
dstIndex++
|
||||
}
|
||||
|
||||
case DELETE:
|
||||
if srcIndex < len(stdoutLines) {
|
||||
diffBuilder.WriteString(fmt.Sprintf("- %s\n", stdoutLines[srcIndex]))
|
||||
srcIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diffBuilder.String()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user