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