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"] | [submodule "examples/cpplint/sillycode"] | ||||||
| 	path = examples/cpplint/sillycode | 	path = examples/cpplint/sillycode | ||||||
| 	url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git | 	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 ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"unicode" | ||||||
| 
 | 
 | ||||||
| 	"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" | 	"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 { | type Conf struct { | ||||||
| 	Cases []struct { | 	Cases []struct { | ||||||
| 		Score      int | 		Outputs []struct { | ||||||
| 		StdoutPath string | 			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) { | 	if len(conf.Cases) != len(results) { | ||||||
| 		return nil, true, fmt.Errorf("cases number not match") | 		return nil, true, fmt.Errorf("cases number not match") | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	var res []stage.ParserResult | 	var res []stage.ParserResult | ||||||
| 	for i, caseConf := range conf.Cases { | 	for i, caseConf := range conf.Cases { | ||||||
| 		result := results[i] | 		result := results[i] | ||||||
| 		score := 0 | 		score := 0 | ||||||
| 		stdout, err := os.ReadFile(caseConf.StdoutPath) | 		comment := "" | ||||||
| 		if err != nil { | 		for _, output := range caseConf.Outputs { | ||||||
| 			return nil, true, err | 			answer, err := os.ReadFile(output.AnswerPath) | ||||||
| 		} | 			if err != nil { | ||||||
| 		// TODO: more compare strategies
 | 				return nil, true, err | ||||||
| 		if string(stdout) == result.Files["stdout"] { | 			} | ||||||
| 			score = caseConf.Score | 
 | ||||||
|  | 			// 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{ | 		res = append(res, stage.ParserResult{ | ||||||
| 			Score: score, | 			Score:   score, | ||||||
| 			Comment: fmt.Sprintf( | 			Comment: comment, | ||||||
| 				"executor status: run time: %d ns, memory: %d bytes", |  | ||||||
| 				result.RunTime, result.Memory, |  | ||||||
| 			), |  | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	return res, false, nil | 	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