Repository: j178/leetgo (language: python)

Issue:
proposal: How to add support of local testing for a new language?
我最近一直在思考一个更加统一、规范的 local testing 的实现，减少支持新语言所需的开发工作量，让各种语言的 local testing 表现更加一致。

以生成 C++ 版本的 `two-sum` 为例，我提议做出以下定义：

## 一个 Generator 应该生成什么？

- question.md [optional]
- testcases.txt
- solution.h
- solution_test.cpp

### `question.md`

`question.md` 包含 Markdown 格式的题目描述，只有当开启 `code.separate_descrption_file` 时才生成这个文件。否则题目描述包含在代码文件中(`solution.h`)。

See #94 .

### `testcases.txt`

`testcases.txt` 由多个 case 组成，case 之间由一个空行分隔。

case 包括 input 和 output 两部分，如果 input 参数有多个，每行放置一个。

示例：
```
target_case: 0

input:
[3,9,20,null,null,15,7]
output:
3

input:
[1,null,2]
output:
2
```

`testcases.txt` 中还可能包含一个可选的 Key-value: `target_case: 0`，表示只执行某一个 case。case 的编号从 1 开始，向下递增。

- `target_case: 0` 中 `0` 表示执行所有 case.
- 也可以为负数，`-1` 代表最后一个 case.


### `solution.h`

主要包含经过模板替换、modifier 修改后的 question code snippet， 示例：
```cpp
// 省略描述部分

// @lc code=begin

class Solution {
public:
    int maxDepth(TreeNode* root) {
        
    }
};

// @lc code=end


```

### `solution_test.cpp`

`solution_test.cpp` 应该是一个完整的程序，每次调用执行一个独立的 case。
- 它从 stdin 读入完整的 case input
- 将 case input 反序列化为函数入参需要的类型，比如 list, list of list, ListNode, TreeNode
- 调用 solution function，获取 function return value
- 将 return value 序列化，从 stdout 输出

`solution_test.cpp` 只是逻辑上的概念，并不一定需要生成为单独的文件，可以与代码文件存在于同一个文件中。

示例：
```cpp
// Code generated by https://github.com/j178/leetgo
#include <bits/stdc++.h>
#include "LC_IO.h"
#include "solution.h"

using namespace std;

int main(int argc, char **argv) {
	// scan input args
	TreeNode* root; cin >> root;

	// initialize object
	Solution *obj = new Solution();

	// call method
	auto &&res = obj->maxDepth(root);

        // print result
	cout << "output: " << res << endl;

	delete obj;
	return 0;
}
```

为避免用户自己打印到 stdout 的内容影响 output 的解析，return value 的 output 应该包含 `output:` 前缀，例如：

```
output: [0,1]
```

序列化和反序列化的功能实现应该存在于一个公共的 library 中，由 `solution_test.cpp` 引用。这个 library 应该由 Generator 的`Initialize()` 拷贝到 `$outDir/common` 中。library 的原始代码应该放在仓库的 `testutils/$lang` 目录下，通过 `embed` 打包在程序中。示例：
<details>
<summary>Click to expand</summary>

```cpp
#ifndef LC_IO_H
#define LC_IO_H

#include <iostream>
#include <queue>

/**
 * Definition for a singly-linked list.
 */
struct ListNode {
	int val;
	ListNode *next;
	ListNode() : val(0), next(nullptr) {}
	ListNode(int x) : val(x), next(nullptr) {}
	ListNode(int x, ListNode *next) : val(x), next(next) {}
};

/**
 * Function for deserializing a singly-linked list.
 */
std::istream &operator>>(std::istream &is, ListNode *&node) {
	node = nullptr;
	ListNode *now = nullptr;
L0: is.ignore();
L1: switch (is.peek()) {
	case ' ':
	case ',': is.ignore(); goto L1;
	case ']': is.ignore(); goto L2;
	default : int x; is >> x;
	          now = (now ? now->next : node) = new ListNode(x);
	          goto L1;
	}
L2: switch (is.peek()) {
	case '\r': is.ignore(); goto L2;
	case '\n': is.ignore(); goto L3;
	case EOF : goto L3;
	}
L3: return is;
}

/**
 * Function for serializing a singly-linked list.
 */
std::ostream &operator<<(std::ostream &os, ListNode *node) {
	os << '[';
	while (node != nullptr) {
		os << node->val << ',';
		node = node->next;
	}
	os.seekp(-1, std::ios_base::end);
	os << ']';
	return os;
}

/**
 * Definition for a binary tree node.
 */
struct TreeNode {
	int val;
	TreeNode *left;
	TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

/**
 * Function for deserializing a binary tree.
 */
std::istream &operator>>(std::istream &is, TreeNode *&node) {
	std::deque<TreeNode *> dq;
L0: is.ignore();
L1: switch (is.peek()) {
	case ' ':
	case ',': is.ignore(); goto L1;
	case 'n': is.ignore(5); dq.emplace_back(nullptr);
	          goto L1;
	case ']': is.ignore(); goto L2;
	default : int x; is >> x;
	          dq.emplace_back(new TreeNode(x));
	          goto L1;
	}
L2: switch (is.peek()) {
	case '\r': is.ignore(); goto L2;
	case '\n': is.ignore(); goto L3;
	case EOF : goto L3;
	}
L3: int n = dq.size();
	for (int i = 0, j = 1; i < n; ++i) {
		auto root = dq[i];
		if (root == nullptr) { continue; }
		root->left = j < n ? dq[j] : nullptr;
		root->right = j + 1 < n ? dq[j + 1] : nullptr;
		j += 2;
	}
	node = n ? dq[0] : nullptr;
	return is;
}

/**
 * Function for serializing a binary tree.
 */
std::ostream &operator<<(std::ostream &os, TreeNode *node) {
	std::queue<TreeNode *> q;
	int cnt_not_null_nodes = 0;
	auto push = [&](TreeNode *node) {
		q.emplace(node);
		if (node != nullptr) { ++cnt_not_null_nodes; }
	};
	auto pop = [&]() {
		auto front = q.front(); q.pop();
		if (front != nullptr) {
			--cnt_not_null_nodes;
			push(front->left);
			push(front->right);
			os << front->val << ',';
		} else {
			os << "null,";
		}
	};
	os << '[';
	if (node != nullptr) {
		push(node);
		while (cnt_not_null_nodes > 0) { pop();	}
		os.seekp(-1, std::ios_base::end);
	}
	os << ']';
	return os;
}

/**
 * Function for deserializing an array.
 */
template <typename T>
std::istream &operator>>(std::istream &is, std::vector<T> &v) {
L0: is.ignore();
L1: switch (is.peek()) {
	case ' ':
	case ',': is.ignore(); goto L1;
	case ']': is.ignore(); goto L2;
	default : v.emplace_back();
	          if constexpr (std::is_same_v<T, std::string>) {
	              is >> quoted(v.back());
	          } else {
	              is >> v.back();
	          }
	          goto L1;
	}
L2: switch (is.peek()) {
	case '\r': is.ignore(); goto L2;
	case '\n': is.ignore(); goto L3;
	case EOF : goto L3;
}
L3: return is;
}

/**
 * Function for serializing an array.
 */
template <typename T>
std::ostream &operator<<(std::ostream &os, const std::vector<T> &v) {
	os << '[';
	if constexpr (std::is_same_v<T, std::string>) {
		for (auto &&x : v) { os << quoted(x) << ','; }
	} else if constexpr (std::is_same_v<T, double>) {
		for (auto &&x : v) {
			char buf[320]; sprintf(buf, "%.5f,", x); os << buf;
		}
	} else {
		for (auto &&x : v) { os << x << ','; }
	}
	os.seekp(-1, std::ios_base::end);
	os << ']';
	return os;
}

#endif
```
</details>

## test generator

这是 `leetgo pick` 的工作，它负责：

 - 初始化测试环境
    
   包括拷贝 library 代码，初始化语言相关的 workspace (`go mod init`, `cargo init`, `python -m venv` 等)
 - 生成 `question.md`、`testcases.txt`、`solution.h`、`solution_test.cpp` 文件
 
## test runner

这是 `leetgo test -L` 的工作，它负责：
1. 编译生成 executable (可选) 

   对于编译型语言，需要现将 `solution_test.cpp` 编译为 binary 才能执行。生成的 binary 应该放在 `/tmp/leetgo/$questionSlug-$langSlug.exec` 。

3. 解析 `testcases.txt` 抽出每个 case 的 input，作为 stdin 调用 executable，捕获 executable 的 stdout 并解析出 output
4. 比对 output 与 expected output（现阶段是直接的字符串比对，后续可能会支持更智能的判断），展示比对结果。

## 支持新语言需要的工作

1. 用新语言实现参数的序列化、反序列化
2. 实现 `solution_test.$lang` 的生成
3. 实现 `solution_test.$lang` 的编译和调用
 
大家怎么看？欢迎一起讨论 @w43322 @frostming @acehinnnqru 



Discussion / hints:
关于solution_test.cpp 我理解你的目的，所有语言都变成一个 stdin -> program -> stdout的测试程序。抹掉了语言的差异，但这样有两个问题：

为了让用户可以专注于逻辑的实现，leetgo需要实现一个调用层
```
       ┌────────────────────────────────────────────┐
       │leetgo                                      │
       │                                            │
       │        ┌─────────────────────────┐         │
       │        │ Language native         │         │
  stdin│        │                         │         │stdout
──────►├────────┼────► class Solution─────┼────────►├─────►
       │        │                         │         │
       │        └─────────────────────────┘         │
       │                                            │
       └────────────────────────────────────────────┘
```
理想情况下用户只需要提供 `class Solution`，那么leetgo则负责完成(read stdin -> decode as language native)，每个语言需要实现一个互操作层

此外，这样把测试放到黑盒里跑了，不利于用户在IDE里打断点调试（指test runner）

不过话说回来，leetcode的测试程序基本也是按这样实现的(stdin, stdout) @yihong0618
是的，一个 docker 的 runtime 但据说要换掉，但这个据说是我两年前听到的
> 理想情况下用户只需要提供 `class Solution`，那么leetgo则负责完成(read stdin -> decode as language native)，每个语言需要实现一个互操作层

`read stdin -> decode as language native` 这应该是 `solution_test.cpp` 自己做的，例如这样：
```cpp
int main(int argc, char **argv) {
	// scan input args
	TreeNode* root; cin >> root;

	// initialize object
	Solution *obj = new Solution();

	// call method
	auto &&res = obj->maxDepth(root);

        // print result
	cout << "$output$" << res << "$output$";

	delete obj;
	return 0;
}
```

leetgo 的调用层应该是：
> 解析 testcases.txt 抽出每个 case 的 input，作为 stdin 调用 executable，捕获 executable 的 stdout 并解析出 output

是与语言无关的。

> 此外，这样把测试放到黑盒里跑了，不利于用户在IDE里打断点调试（指test runner）

因为 `solution_test.cpp` 是一个完整的程序，所以用户依然是可以打断点调试的。只是这种情况下没有 leetgo 提供 stdin input，用户需要自己复制粘贴 case 到控制台中。

关于这个问题我有一些想法，但最近比较忙，下周我细说一下

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9b76103f..09db4dfe 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -8,6 +8,8 @@ jobs:
   ci:
     name: CI
     runs-on: ubuntu-latest
+    # Prevent duplicate builds on internal PRs.
+    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
     steps:
       - uses: actions/checkout@v3
       - uses: actions/setup-go@v3
diff --git a/README.md b/README.md
index 7aca5242..234b5167 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,30 @@ And `leetgo` also supports real-time generation of **contest questions**, submit
 
 `leetgo` supports code generation for most languages, and local testing for some languages.
 
+In the Go language, running `leetgo pick 257` will generate the following code:
+
+```go
+// Omitted some code...
+// @lc code=begin
+
+func binaryTreePaths(root *TreeNode) (ans []string) {
+
+	return
+}
+
+// @lc code=end
+
+func main() {
+	stdin := bufio.NewReader(os.Stdin)
+	root := Deserialize[*TreeNode](ReadLine(stdin))
+	ans := binaryTreePaths(root)
+	fmt.Println("output: " + Serialize(ans))
+}
+```
+
+This is a complete and runnable program. You can run it directly, input the test cases, and compare the results. 
+`leetgo test -L` will automatically run this program with the test cases in `testcases.txt` and compare the results.
+
 Local testing means that you can run the test cases on your local machine, so you can use a debugger to debug your code.
 
 Local testing requires more work to implement for each language, so not all languages are supported.
@@ -167,21 +191,12 @@ code:
     out_dir: go
     # Overrides the default code.filename_template
     filename_template: ""
-    # Replace some blocks of the generated code
-    blocks:
-      - name: beforeMarker
-        template: |
-          package main
-
-          {{ if .NeedsDefinition -}} import . "github.com/j178/leetgo/testutils/go" {{- end }}
     # Functions that modify the generated code
     modifiers:
       - name: removeUselessComments
       - name: changeReceiverName
       - name: addNamedReturn
       - name: addMod
-    # Go module path for the generated code
-    go_mod_path: ""
   python3:
     out_dir: python
     # Overrides the default code.filename_template
diff --git a/README_zh.md b/README_zh.md
index dc173360..8595eb64 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -31,6 +31,29 @@
 
 `leetgo` 可以为大多数语言生成样例代码，以及为部分语言生成本地测试代码。
 
+以 Go 语言为例，`leetgo pick 257` 会生成如下代码：
+
+```go
+// 省略一些代码...
+// @lc code=begin
+
+func binaryTreePaths(root *TreeNode) (ans []string) {
+
+	return
+}
+
+// @lc code=end
+
+func main() {
+	stdin := bufio.NewReader(os.Stdin)
+	root := Deserialize[*TreeNode](ReadLine(stdin))
+	ans := binaryTreePaths(root)
+	fmt.Println("output: " + Serialize(ans))
+}
+```
+
+这是一个完整的可运行的程序，你可以直接运行它，输入测试样例，比对结果。`leetgo test -L` 会自动按照 `testcases.txt` 中的 case 运行这个程序，并且比对结果。
+
 本地测试意味着你可以在你的机器上运行你的代码，输入测试样例比对结果，你可以使用 Debugger 来单步调试你的代码，更容易的找出代码中的问题。
 
 本地测试需要为每一种语言做单独的适配，所以目前仅支持部分语言(其实只支持 Go)，下表是目前的支持情况：
@@ -162,21 +185,12 @@ code:
     out_dir: go
     # Overrides the default code.filename_template
     filename_template: ""
-    # Replace some blocks of the generated code
-    blocks:
-      - name: beforeMarker
-        template: |
-          package main
-
-          {{ if .NeedsDefinition -}} import . "github.com/j178/leetgo/testutils/go" {{- end }}
     # Functions that modify the generated code
     modifiers:
       - name: removeUselessComments
       - name: changeReceiverName
       - name: addNamedReturn
       - name: addMod
-    # Go module path for the generated code
-    go_mod_path: ""
   python3:
     out_dir: python
     # Overrides the default code.filename_template
diff --git a/cmd/edit.go b/cmd/edit.go
index 71681a6f..e8952a05 100644
--- a/cmd/edit.go
+++ b/cmd/edit.go
@@ -28,7 +28,7 @@ var editCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		return editor.Open(result.Files)
+		return editor.Open(result)
 	},
 }
 
diff --git a/cmd/git.go b/cmd/git.go
index 6319aa2c..fa07b79b 100644
--- a/cmd/git.go
+++ b/cmd/git.go
@@ -50,7 +50,7 @@ func init() {
 func gitAddCommitPush(genResult *lang.GenerateResult) error {
 	files := make([]string, 0, len(genResult.Files))
 	for _, f := range genResult.Files {
-		files = append(files, f.Path)
+		files = append(files, f.GetPath())
 	}
 	err := runCmd("git", "add", files...)
 	if err != nil {
diff --git a/cmd/pick.go b/cmd/pick.go
index c77d6bc9..2dd21d28 100644
--- a/cmd/pick.go
+++ b/cmd/pick.go
@@ -130,7 +130,7 @@ leetgo pick two-sum`,
 		if err != nil {
 			return err
 		}
-		err = editor.Open(result.Files)
+		err = editor.Open(result)
 		return err
 	},
 }
diff --git a/config/config.go b/config/config.go
index 54c81295..78849cad 100644
--- a/config/config.go
+++ b/config/config.go
@@ -101,7 +101,6 @@ type BaseLangConfig struct {
 
 type GoConfig struct {
 	BaseLangConfig `yaml:",inline" mapstructure:",squash"`
-	GoModPath      string `yaml:"go_mod_path" mapstructure:"go_mod_path" comment:"Go module path for the generated code"`
 }
 
 type Credentials struct {
@@ -193,16 +192,6 @@ func Default() *Config {
 			Go: GoConfig{
 				BaseLangConfig: BaseLangConfig{
 					OutDir: "go",
-					Blocks: []Block{
-						{
-							Name: "beforeMarker", Template: fmt.Sprintf(
-								`package main
-
-{{ if .NeedsDefinition -}} import . "%s" {{- end }}
-`, GoTestUtilsModPath,
-							),
-						},
-					},
 					Modifiers: []Modifier{
 						{Name: "removeUselessComments"},
 						{Name: "changeReceiverName"},
diff --git a/editor/editor.go b/editor/editor.go
index 53bee94f..0e475011 100644
--- a/editor/editor.go
+++ b/editor/editor.go
@@ -12,12 +12,12 @@ import (
 )
 
 type Opener interface {
-	Open(file lang.FileOutput) error
+	Open(result *lang.GenerateResult) error
 }
 
 type MultiOpener interface {
 	Opener
-	OpenMulti(files []lang.FileOutput) error
+	OpenMulti(result *lang.GenerateResult) error
 }
 
 var editors = map[string]Opener{
@@ -41,7 +41,7 @@ var editors = map[string]Opener{
 
 type noneEditor struct{}
 
-func (e *noneEditor) Open(file lang.FileOutput) error {
+func (e *noneEditor) Open(result *lang.GenerateResult) error {
 	log.Info("none editor is used, skip opening files")
 	return nil
 }
@@ -51,19 +51,19 @@ type commonEditor struct {
 	args    []string
 }
 
-func (e *commonEditor) Open(file lang.FileOutput) error {
+func (e *commonEditor) Open(result *lang.GenerateResult) error {
 	log.Info("opening file", "command", e.command)
-	return runCmd(e.command, e.args, file.Path)
+	return runCmd(e.command, e.args, result.GetFile(lang.CodeFile).GetPath())
 }
 
 type commonMultiEditor struct {
 	commonEditor
 }
 
-func (e *commonMultiEditor) OpenMulti(files []lang.FileOutput) error {
-	paths := make([]string, len(files))
-	for i, f := range files {
-		paths[i] = f.Path
+func (e *commonMultiEditor) OpenMulti(result *lang.GenerateResult) error {
+	paths := make([]string, 0, len(result.Files))
+	for _, f := range result.Files {
+		paths = append(paths, f.GetPath())
 	}
 	log.Info("opening files", "command", e.command)
 	return runCmd(e.command, e.args, paths...)
@@ -71,26 +71,29 @@ func (e *commonMultiEditor) OpenMulti(files []lang.FileOutput) error {
 
 type customEditor struct{}
 
-func (e *customEditor) Open(file lang.FileOutput) error {
+func (e *customEditor) Open(result *lang.GenerateResult) error {
 	cfg := config.Get()
 	if cfg.Editor.Command == "" {
 		log.Warn("editor.command is empty, skip opening files")
 		return nil
 	}
 	log.Info("opening files", "command", cfg.Editor.Command)
-	return runCmd(cfg.Editor.Command, cfg.Editor.Args, file.Path)
+	return runCmd(cfg.Editor.Command, cfg.Editor.Args, result.GetFile(lang.CodeFile).GetPath())
 }
 
 func Get(s string) Opener {
 	return editors[s]
 }
 
-func Open(paths []lang.FileOutput) error {
-	if len(paths) == 0 {
+func Open(result *lang.GenerateResult) error {
+	if len(result.Files) == 0 {
 		return nil
 	}
-	cfg := config.Get()
+	if result.GetFile(lang.CodeFile) == nil {
+		return fmt.Errorf("no code file found")
+	}
 
+	cfg := config.Get()
 	ed := Get(cfg.Editor.Use)
 	if ed == nil {
 		return fmt.Errorf(
@@ -99,9 +102,9 @@ func Open(paths []lang.FileOutput) error {
 		)
 	}
 	if ed, ok := ed.(MultiOpener); ok {
-		return ed.OpenMulti(paths)
+		return ed.OpenMulti(result)
 	}
-	return ed.Open(paths[0])
+	return ed.Open(result)
 }
 
 func runCmd(command string, args []string, files ...string) error {
diff --git a/go.mod b/go.mod
index 4a447975..4d9be2a5 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/hexops/gotextdiff v1.0.3
 	github.com/ivanpirog/coloredcobra v1.0.1
 	github.com/j178/kooky v0.0.0-20230225091234-c0b5404b0b88
+	github.com/j178/leetgo/testutils/go v0.1.0-alpha
 	github.com/jedib0t/go-pretty/v6 v6.4.4
 	github.com/k3a/html2text v1.1.0
 	github.com/mattn/go-sqlite3 v1.14.16
@@ -69,6 +70,7 @@ require (
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/microcosm-cc/bluemonday v1.0.21 // indirect
+	github.com/mitchellh/go-ps v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
diff --git a/go.sum b/go.sum
index ede6403f..51d0ea6f 100644
--- a/go.sum
+++ b/go.sum
@@ -229,12 +229,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLfbgNxrN4=
 github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q=
-github.com/j178/kooky v0.0.0-20230225090305-5bc7062de47d h1:jFI9Jy2PG6WPjAGoxi5WRuPiF8uu0gC7f6wzf3zE0us=
-github.com/j178/kooky v0.0.0-20230225090305-5bc7062de47d/go.mod h1:7Rm3VdbhA/02fT1ObHhffCHXDhdmn3/x+EIHCbMuf44=
-github.com/j178/kooky v0.0.0-20230225091032-0ebed9201315 h1:TT6kEH3xOaY3SMXJuFMCeplHWlVwz7pqq8941dEV37I=
-github.com/j178/kooky v0.0.0-20230225091032-0ebed9201315/go.mod h1:7Rm3VdbhA/02fT1ObHhffCHXDhdmn3/x+EIHCbMuf44=
 github.com/j178/kooky v0.0.0-20230225091234-c0b5404b0b88 h1:x0BMg5C+3E4UuZ0t5R5BsEzSnpjlV6bhmn7oSWJ0/PY=
 github.com/j178/kooky v0.0.0-20230225091234-c0b5404b0b88/go.mod h1:7Rm3VdbhA/02fT1ObHhffCHXDhdmn3/x+EIHCbMuf44=
+github.com/j178/leetgo/testutils/go v0.1.0-alpha h1:mXse8WbvKWeMBtsdOk3KWXDzGh5e4Q1NyPpFo+EZodM=
+github.com/j178/leetgo/testutils/go v0.1.0-alpha/go.mod h1:Nwd7mblpQsRDCXay0vw206Tqocrxph9enVukW8nkgn4=
 github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc=
 github.com/jedib0t/go-pretty/v6 v6.4.4/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI=
 github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
@@ -293,6 +291,8 @@ github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPw
 github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
diff --git a/go.work b/go.work
new file mode 100644
index 00000000..d7c318b0
--- /dev/null
+++ b/go.work
@@ -0,0 +1,6 @@
+go 1.20
+
+use (
+	.
+	testutils/go
+)
diff --git a/lang/base.go b/lang/base.go
index b8c2fb72..1f8124c0 100644
--- a/lang/base.go
+++ b/lang/base.go
@@ -2,12 +2,16 @@ package lang
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
+	"os"
 	"path/filepath"
 	"strings"
 	"text/template"
 	"time"
 
+	"github.com/charmbracelet/log"
+	"github.com/dop251/goja"
 	"github.com/spf13/viper"
 
 	"github.com/j178/leetgo/config"
@@ -15,6 +19,95 @@ import (
 	"github.com/j178/leetgo/utils"
 )
 
+const (
+	testCaseInputMark  = "input:"
+	testCaseOutputMark = "output:"
+	testCaseTargetMark = "target_case:"
+)
+
+type GenerateResult struct {
+	Question *leetcode.QuestionData
+	Lang     Lang
+	OutDir   string
+	SubDir   string
+	Files    []FileOutput
+	mask     int
+}
+
+type FileOutput struct {
+	genResult *GenerateResult
+	Filename  string
+	Type      FileType
+	Content   string
+	Written   bool
+}
+
+func (f *FileOutput) GetPath() string {
+	return filepath.Join(f.genResult.OutDir, f.genResult.SubDir, f.Filename)
+}
+
+func (f *FileOutput) GetContent() (string, error) {
+	if f.Content == "" {
+		content, err := os.ReadFile(f.GetPath())
+		if err != nil {
+			return "", err
+		}
+		f.Content = string(content)
+	}
+	return f.Content, nil
+}
+
+type FileType int
+
+const (
+	CodeFile FileType = 1 << iota
+	TestFile
+	TestCasesFile
+	DocFile
+	OtherFile
+)
+
+func (r *GenerateResult) AddFile(f FileOutput) *GenerateResult {
+	if r.mask&int(f.Type) != 0 {
+		panic(fmt.Sprintf("file type %d already exists", f.Type))
+	}
+	f.genResult = r
+	r.Files = append(r.Files, f)
+	r.mask |= int(f.Type)
+	return r
+}
+
+func (r *GenerateResult) GetFile(typ FileType) *FileOutput {
+	for _, f := range r.Files {
+		if int(f.Type&typ) != 0 {
+			return &f
+		}
+	}
+	return nil
+}
+
+func (r *GenerateResult) SetOutDir(dir string) {
+	r.OutDir = dir
+}
+
+type Lang interface {
+	Name() string
+	ShortName() string
+	Slug() string
+	// Generate generates code files for the question.
+	Generate(q *leetcode.QuestionData) (*GenerateResult, error)
+	GeneratePaths(q *leetcode.QuestionData) (*GenerateResult, error)
+}
+
+type NeedInitialization interface {
+	HasInitialized(dir string) (bool, error)
+	Initialize(dir string) error
+}
+
+type LocalTestable interface {
+	RunLocalTest(q *leetcode.QuestionData, dir string) (bool, error)
+}
+
 func getCodeStringConfig(lang Lang, key string) string {
 	ans := viper.GetString("code." + lang.Slug() + "." + key)
 	if ans != "" {
@@ -60,6 +153,176 @@ func separateDescriptionFile(lang Lang) bool {
 	return config.Get().Code.SeparateDescriptionFile
 }
 
+const codeContentTemplate = `
+{{- block "header" . -}}
+{{ .LineComment }} Created by {{ .Author }} at {{ .Time }}
+{{ .LineComment }} {{ .Question.Url }}
+{{ if .Question.IsContest }}{{ .LineComment }} {{ .Question.ContestUrl }}
+{{ end }}
+{{ end }}
+{{ if not .SeparateDescriptionFile }}
+{{ block "description" . -}}
+{{ .BlockCommentStart }}
+{{ block "title" . }}{{ .Question.QuestionFrontendId }}. {{ .Question.GetTitle }} ({{ .Question.Difficulty }}){{ end }}
+{{ .Question.GetFormattedContent }}
+{{ .BlockCommentEnd }}
+{{ end }}
+{{ end }}
+{{ block "_internalBeforeMarker" . }}{{ end }}
+{{ block "beforeMarker" . }}{{ end }}
+{{ .LineComment }} {{ .CodeBeginMarker }}
+{{ block "beforeCode" . }}{{ end }}
+{{ block "code" . }}{{ .Code | runModifiers }}{{ end }}
+{{ block "afterCode" . }}{{ end }}
+{{ .LineComment }} {{ .CodeEndMarker }}
+{{ block "afterMarker" . }}{{ end }}
+{{ block "_internalAfterMarker" . }}{{ end }}
+`
+
+type codeContentData struct {
+	Question                *leetcode.QuestionData
+	Author                  string
+	Time                    string
+	LineComment             string
+	BlockCommentStart       string
+	BlockCommentEnd         string
+	CodeBeginMarker         string
+	CodeEndMarker           string
+	Code                    string
+	SeparateDescriptionFile bool
+	NeedsDefinition         bool
+}
+
+var validBlocks = map[string]bool{
+	"header":       true,
+	"description":  true,
+	"title":        true,
+	"beforeMarker": true,
+	"beforeCode":   true,
+	"code":         true,
+	"afterCode":    true,
+	"afterMarker":  true,
+}
+
+// internal blocks are used to generate code for internal use.
+const (
+	internalBeforeMarker = "_internalBeforeMarker"
+	internalAfterMarker  = "_internalAfterMarker"
+)
+
+var internalBlocks = map[string]bool{
+	internalBeforeMarker: true,
+	internalAfterMarker:  true,
+}
+
+var builtinModifiers = map[string]ModifierFunc{
+	"removeUselessComments": removeUselessComments,
+}
+
+type ModifierFunc = func(string, *leetcode.QuestionData) string
+
+func getBlocks(lang Lang) (ans []config.Block) {
+	blocks := viper.Get("code." + lang.Slug() + ".blocks")
+	if blocks == nil || len(blocks.([]any)) == 0 {
+		blocks = viper.Get("code." + lang.ShortName() + ".blocks")
+	}
+	if blocks == nil || len(blocks.([]any)) == 0 {
+		blocks = viper.Get("code.blocks")
+	}
+	if blocks == nil {
+		return
+	}
+	for _, b := range blocks.([]any) {
+		ans = append(
+			ans, config.Block{
+				Name:     b.(map[string]any)["name"].(string),
+				Template: b.(map[string]any)["template"].(string),
+			},
+		)
+	}
+	return
+}
+
+func getModifiers(lang Lang, modifiersMap map[string]ModifierFunc) ([]ModifierFunc, error) {
+	modifiers := viper.Get("code." + lang.Slug() + ".modifiers")
+	if modifiers == nil || len(modifiers.([]any)) == 0 {
+		modifiers = viper.Get("code." + lang.ShortName() + ".modifiers")
+	}
+	if modifiers == nil || len(modifiers.([]any)) == 0 {
+		modifiers = viper.Get("code.modifiers")
+	}
+	if modifiers == nil {
+		return nil, nil
+	}
+
+	var funcs []ModifierFunc
+	for _, m := range modifiers.([]any) {
+		m := m.(map[string]any)
+		name, script := "", ""
+
+		if m["name"] != nil {
+			name = m["name"].(string)
+			if f, ok := modifiersMap[name]; ok {
+				funcs = append(funcs, f)
+				continue
+			}
+		}
+		if m["script"] != nil {
+			script = m["script"].(string)
+			vm := goja.New()
+			_, err := vm.RunString(script)
+			if err != nil {
+				return nil, fmt.Errorf("failed to run script: %w", err)
+			}
+			var jsFn func(string) string
+			if vm.Get("modify") == nil {
+				return nil, fmt.Errorf("failed to get modify function")
+			}
+			err = vm.ExportTo(vm.Get("modify"), &jsFn)
+			if err != nil {
+				return nil, fmt.Errorf("failed to export function: %w", err)
+			}
+			f := func(s string, data *leetcode.QuestionData) string {
+				return jsFn(s)
+			}
+			funcs = append(funcs, f)
+			continue
+		}
+		log.Warn("invalid modifier, ignored", "name", name, "script", script)
+	}
+	return funcs, nil
+}
+
+func needsDefinition(code string) bool {
+	return strings.Contains(code, "Definition for")
+}
+
+func needsMod(content string) bool {
+	return strings.Contains(content, "<code>10<sup>9</sup> + 7</code>") || strings.Contains(content, "10^9 + 7")
+}
+
+func removeUselessComments(code string, q *leetcode.QuestionData) string {
+	lines := strings.Split(code, "\n")
+	var newLines []string
+	for i := 0; i < len(lines); i++ {
+		line := lines[i]
+		if strings.HasPrefix(line, "/**") && (strings.Contains(
+			lines[i+1],
+			"object will be instantiated and called",
+		) || strings.Contains(lines[i+1], "Definition for")) {
+			for {
+				i++
+				if strings.HasSuffix(lines[i], "*/") {
+					break
+				}
+			}
+			continue
+		}
+		newLines = append(newLines, line)
+	}
+	return strings.Join(newLines, "\n")
+}
+
 type baseLang struct {
 	name              string
 	slug              string
@@ -82,13 +345,8 @@ func (l baseLang) ShortName() string {
 	return l.shortName
 }
 
-func (l baseLang) LineComment() string {
-	return l.lineComment
-}
-
 func (l baseLang) generateCodeContent(
 	q *leetcode.QuestionData,
-	baseFilename string,
 	blocks []config.Block,
 	modifiers []ModifierFunc,
 	separateDescriptionFile bool,
@@ -105,18 +363,12 @@ func (l baseLang) generateCodeContent(
 			},
 		},
 	)
-	var err error
-	if separateDescriptionFile {
-		_, err = tmpl.Parse(withoutDescriptionContentTemplate)
-	} else {
-		_, err = tmpl.Parse(defaultContentTemplate)
-	}
+	_, err := tmpl.Parse(codeContentTemplate)
 	if err != nil {
 		return "", err
 	}
-
 	for _, block := range blocks {
-		if _, ok := validBlocks[block.Name]; !ok {
+		if !validBlocks[block.Name] && !internalBlocks[block.Name] {
 			return "", fmt.Errorf("invalid block name: %s", block.Name)
 		}
 		_, err := tmpl.New(block.Name).Parse(block.Template)
@@ -126,17 +378,18 @@ func (l baseLang) generateCodeContent(
 	}
 
 	cfg := config.Get()
-	data := &contentData{
-		Question:          q,
-		Author:            cfg.Author,
-		Time:              time.Now().Format("2006/01/02 15:04"),
-		LineComment:       l.lineComment,
-		BlockCommentStart: l.blockCommentStart,
-		BlockCommentEnd:   l.blockCommentEnd,
-		CodeBeginMarker:   config.CodeBeginMarker,
-		CodeEndMarker:     config.CodeEndMarker,
-		Code:              code,
-		NeedsDefinition:   needsDefinition(code),
+	data := &codeContentData{
+		Question:                q,
+		Author:                  cfg.Author,
+		Time:                    time.Now().Format("2006/01/02 15:04"),
+		LineComment:             l.lineComment,
+		BlockCommentStart:       l.blockCommentStart,
+		BlockCommentEnd:         l.blockCommentEnd,
+		CodeBeginMarker:         config.CodeBeginMarker,
+		CodeEndMarker:           config.CodeEndMarker,
+		Code:                    code,
+		SeparateDescriptionFile: separateDescriptionFile,
+		NeedsDefinition:         needsDefinition(code),
 	}
 	var buf bytes.Buffer
 	err = tmpl.Execute(&buf, data)
@@ -151,7 +404,7 @@ func (l baseLang) generateCodeContent(
 
 func (l baseLang) generateCodeFile(
 	q *leetcode.QuestionData,
-	baseFilename string,
+	filename string,
 	blocks []config.Block,
 	modifiers []ModifierFunc,
 	separateDescriptionFile bool,
@@ -161,7 +414,6 @@ func (l baseLang) generateCodeFile(
 ) {
 	content, err := l.generateCodeContent(
 		q,
-		baseFilename,
 		blocks,
 		modifiers,
 		separateDescriptionFile,
@@ -170,21 +422,16 @@ func (l baseLang) generateCodeFile(
 		return FileOutput{}, err
 	}
 	return FileOutput{
-		Path:    baseFilename + l.extension,
-		Content: content,
-		Type:    CodeFile,
+		Filename: filename,
+		Content:  content,
+		Type:     CodeFile,
 	}, nil
 }
 
-func (l baseLang) generateTestCases(q *leetcode.QuestionData) string {
+func (l baseLang) generateTestCasesContent(q *leetcode.QuestionData) string {
 	cases := q.GetTestCases()
 	outputs := q.ParseExampleOutputs()
-	argsNum := 0
-	if q.MetaData.SystemDesign {
-		argsNum = 2
-	} else {
-		argsNum = len(q.MetaData.Params)
-	}
+	argsNum := q.MetaData.NArg()
 
 	// Assume all questions output are single.
 	var caseAndOutputs []string
@@ -195,10 +442,27 @@ func (l baseLang) generateTestCases(q *leetcode.QuestionData) string {
 			fmt.Sprintf("%s\n%s\n%s\n%s", testCaseInputMark, input, testCaseOutputMark, outputs[i/argsNum]),
 		)
 	}
-	return strings.Join(caseAndOutputs, "\n\n")
+	content := strings.Join(caseAndOutputs, "\n\n")
+	content = utils.EnsureTrailingNewline(content)
+	return content
+}
+
+func (l baseLang) generateTestCasesFile(q *leetcode.QuestionData, filename string) (FileOutput, error) {
+	content := l.generateTestCasesContent(q)
+	content = fmt.Sprintf("%s 0\n\n", testCaseTargetMark) + content
+	return FileOutput{
+		Filename: filename,
+		Content:  content,
+		Type:     TestCasesFile,
+	}, nil
 }
 
-func (l baseLang) generateDescriptionFile(q *leetcode.QuestionData, baseFilename string) (FileOutput, error) {
+// nolint: unused
+func (l baseLang) generateTestFile(q *leetcode.QuestionData, filename string) (FileOutput, error) {
+	return FileOutput{}, errors.New("not implemented")
+}
+
+func (l baseLang) generateDescriptionFile(q *leetcode.QuestionData, filename string) (FileOutput, error) {
 	tmpl := `# [%s. %s](%s) (%s)
 %s`
 	url := ""
@@ -216,9 +480,9 @@ func (l baseLang) generateDescriptionFile(q *leetcode.QuestionData, baseFilename
 		q.GetFormattedContent(),
 	)
 	return FileOutput{
-		Path:    baseFilename + ".md",
-		Content: content,
-		Type:    DocFile,
+		Filename: filename,
+		Content:  content,
+		Type:     DocFile,
 	}, nil
 }
 
@@ -229,26 +493,25 @@ func (l baseLang) GeneratePaths(q *leetcode.QuestionData) (*GenerateResult, erro
 		return nil, err
 	}
 
-	code := FileOutput{
-		Path: baseFilename + l.extension,
-		Type: CodeFile,
+	genResult := &GenerateResult{
+		Question: q,
+		Lang:     l,
 	}
-	files := []FileOutput{code}
-
+	genResult.AddFile(
+		FileOutput{
+			Filename: baseFilename + l.extension,
+			Type:     CodeFile,
+		},
+	)
 	if separateDescriptionFile(l) {
-		files = append(
-			files, FileOutput{
-				Path: baseFilename + ".md",
-				Type: DocFile,
+		genResult.AddFile(
+			FileOutput{
+				Filename: baseFilename + ".md",
+				Type:     DocFile,
 			},
 		)
 	}
-
-	return &GenerateResult{
-		Question: q,
-		Lang:     l,
-		Files:    files,
-	}, nil
+	return genResult, nil
 }
 
 func (l baseLang) Generate(q *leetcode.QuestionData) (*GenerateResult, error) {
@@ -258,28 +521,29 @@ func (l baseLang) Generate(q *leetcode.QuestionData) (*GenerateResult, error) {
 		return nil, err
 	}
 
+	genResult := &GenerateResult{
+		Question: q,
+		Lang:     l,
+	}
+
 	separateDescriptionFile := separateDescriptionFile(l)
 	blocks := getBlocks(l)
 	modifiers, err := getModifiers(l, builtinModifiers)
 	if err != nil {
 		return nil, err
 	}
-	codeFile, err := l.generateCodeFile(q, baseFilename, blocks, modifiers, separateDescriptionFile)
+	codeFile, err := l.generateCodeFile(q, baseFilename+l.extension, blocks, modifiers, separateDescriptionFile)
 	if err != nil {
 		return nil, err
 	}
-	files := []FileOutput{codeFile}
+	genResult.AddFile(codeFile)
 
 	if separateDescriptionFile {
-		docFile, err := l.generateDescriptionFile(q, baseFilename)
+		docFile, err := l.generateDescriptionFile(q, baseFilename+".md")
 		if err != nil {
 			return nil, err
 		}
-		files = append(files, docFile)
+		genResult.AddFile(docFile)
 	}
-	return &GenerateResult{
-		Question: q,
-		Lang:     l,
-		Files:    files,
-	}, nil
+	return genResult, nil
 }
diff --git a/lang/gen.go b/lang/gen.go
index 977f6d64..c7c64428 100644
--- a/lang/gen.go
+++ b/lang/gen.go
@@ -3,7 +3,6 @@ package lang
 import (
 	"fmt"
 	"os"
-	"path/filepath"
 	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
@@ -15,67 +14,6 @@ import (
 	"github.com/j178/leetgo/utils"
 )
 
-const (
-	testCaseInputMark  = "input:"
-	testCaseOutputMark = "output:"
-)
-
-type GenerateResult struct {
-	Question *leetcode.QuestionData
-	Lang     Lang
-	Files    []FileOutput
-}
-
-type FileOutput struct {
-	Path    string
-	Content string
-	Written bool
-	Type    FileType
-}
-
-type FileType int
-
-const (
-	CodeFile FileType = iota
-	TestFile
-	DocFile
-	OtherFile
-)
-
-func (r *GenerateResult) GetCodeFile() *FileOutput {
-	for _, f := range r.Files {
-		if f.Type == CodeFile {
-			return &f
-		}
-	}
-	return nil
-}
-
-func (r *GenerateResult) PrependPath(dir string) {
-	for i, f := range r.Files {
-		r.Files[i].Path = filepath.Join(dir, f.Path)
-	}
-}
-
-type Lang interface {
-	Name() string
-	ShortName() string
-	Slug() string
-	LineComment() string
-	// Generate generates code files for the question.
-	Generate(q *leetcode.QuestionData) (*GenerateResult, error)
-	GeneratePaths(q *leetcode.QuestionData) (*GenerateResult, error)
-}
-
-type NeedInitialization interface {
-	HasInitialized(dir string) (bool, error)
-	Initialize(dir string) error
-}
-
-type LocalTestable interface {
-	RunLocalTest(q *leetcode.QuestionData, dir string) (bool, error)
-}
-
 func GetGenerator(lang string) (Lang, error) {
 	lang = strings.ToLower(lang)
 	for _, l := range SupportedLangs {
@@ -142,12 +80,12 @@ func generate(q *leetcode.QuestionData) (Lang, *GenerateResult, error) {
 	if err != nil {
 		return nil, nil, err
 	}
-	result.PrependPath(outDir)
+	result.SetOutDir(outDir)
 	// Write files
 	for i, file := range result.Files {
-		written, err := tryWrite(file.Path, file.Content)
+		written, err := tryWrite(file.GetPath(), file.Content)
 		if err != nil {
-			log.Error("failed to write file", "path", file.Path, "err", err)
+			log.Error("failed to write file", "path", file.GetPath(), "err", err)
 			continue
 		}
 		result.Files[i].Written = written
@@ -240,7 +178,7 @@ func GeneratePathsOnly(q *leetcode.QuestionData) (*GenerateResult, error) {
 	}
 
 	outDir := getOutDir(q, gen)
-	result.PrependPath(outDir)
+	result.SetOutDir(outDir)
 	return result, nil
 }
 
@@ -249,19 +187,15 @@ func GetSolutionCode(q *leetcode.QuestionData) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	codeFile := result.GetCodeFile()
+	codeFile := result.GetFile(CodeFile)
 	if codeFile == nil {
 		return "", fmt.Errorf("no code file generated")
 	}
-	if !utils.IsExist(codeFile.Path) {
-		return "", fmt.Errorf("code file %s does not exist", codeFile.Path)
-	}
-	code, err := os.ReadFile(codeFile.Path)
+	code, err := codeFile.GetContent()
 	if err != nil {
 		return "", err
 	}
-
-	codeLines := strings.Split(string(code), "\n")
+	codeLines := strings.Split(code, "\n")
 	var codeLinesToKeep []string
 	inCode := false
 	for _, line := range codeLines {
@@ -284,7 +218,7 @@ func GetSolutionCode(q *leetcode.QuestionData) (string, error) {
 		}
 	}
 	if nonEmptyLines == 0 {
-		return "", fmt.Errorf("no code found in %s", codeFile.Path)
+		return "", fmt.Errorf("no code found in %s", codeFile.GetPath())
 	}
 
 	return strings.Join(codeLinesToKeep, "\n"), nil
@@ -295,19 +229,15 @@ func UpdateSolutionCode(q *leetcode.QuestionData, newCode string) error {
 	if err != nil {
 		return err
 	}
-	codeFile := result.GetCodeFile()
+	codeFile := result.GetFile(CodeFile)
 	if codeFile == nil {
 		return fmt.Errorf("no code file generated")
 	}
-	if !utils.IsExist(codeFile.Path) {
-		return fmt.Errorf("code file %s does not exist", codeFile.Path)
-	}
-	code, err := os.ReadFile(codeFile.Path)
+	code, err := codeFile.GetContent()
 	if err != nil {
 		return err
 	}
-
-	lines := strings.Split(string(code), "\n")
+	lines := strings.Split(code, "\n")
 	var newLines []string
 	skip := false
 	for _, line := range lines {
@@ -324,30 +254,10 @@ func UpdateSolutionCode(q *leetcode.QuestionData, newCode string) error {
 	}
 
 	newContent := strings.Join(newLines, "\n")
-	err = os.WriteFile(codeFile.Path, []byte(newContent), 0o644)
+	err = os.WriteFile(codeFile.GetPath(), []byte(newContent), 0o644)
 	if err != nil {
 		return err
 	}
-	log.Info("updated", "file", utils.RelToCwd(codeFile.Path))
+	log.Info("updated", "file", utils.RelToCwd(codeFile.GetPath()))
 	return nil
 }
-
-func RunLocalTest(q *leetcode.QuestionData) (bool, error) {
-	cfg := config.Get()
-	gen, err := GetGenerator(cfg.Code.Lang)
-	if err != nil {
-		return false, err
-	}
-
-	tester, ok := gen.(LocalTestable)
-	if !ok {
-		return false, fmt.Errorf("language %s does not support local test", gen.Slug())
-	}
-
-	outDir := getOutDir(q, gen)
-	if !utils.IsExist(outDir) {
-		return false, fmt.Errorf("no code generated for %s in language %s", q.TitleSlug, gen.Slug())
-	}
-
-	return tester.RunLocalTest(q, outDir)
-}
diff --git a/lang/go.go b/lang/go.go
index 97958523..9b5d8cc8 100644
--- a/lang/go.go
+++ b/lang/go.go
@@ -6,39 +6,12 @@ import (
 	"io"
 	"os"
 	"os/exec"
-	"path/filepath"
 	"strings"
 
-	"github.com/charmbracelet/log"
 	"github.com/j178/leetgo/config"
 	"github.com/j178/leetgo/leetcode"
 )
 
-const (
-	testFileTemplate = `// Code generated by %s.
-
-package main
-
-import (
-    "testing"
-
-    . "%s"
-)
-
-var testcases = ` + "`" + `
-%s
-` + "`" + `
-
-func Test_%s(t *testing.T) {
-    targetCaseNum := 0
-    // targetCaseNum := -1
-    if err := %s(t, %s, testcases, targetCaseNum); err != nil {
-        t.Fatal(err)
-    }
-}
-`
-)
-
 type golang struct {
 	baseLang
 }
@@ -141,14 +114,11 @@ func (g golang) HasInitialized(outDir string) (bool, error) {
 }
 
 func (g golang) Initialize(outDir string) error {
-	modPath := config.Get().Code.Go.GoModPath
-	if modPath == "" {
-		modPath = "leetcode-solutions"
-		log.Warn("`code.go.go_mod_path` is not set, use default path", "mod_path", modPath)
-	}
+	modPath := "leetcode-solutions"
 	var stderr bytes.Buffer
 	cmd := exec.Command("go", "mod", "init", modPath)
 	cmd.Dir = outDir
+	cmd.Stdout = os.Stdout
 	cmd.Stderr = io.MultiWriter(os.Stderr, &stderr)
 	err := cmd.Run()
 	if err != nil && !bytes.Contains(stderr.Bytes(), []byte("go.mod already exists")) {
@@ -164,36 +134,174 @@ func (g golang) Initialize(outDir string) error {
 }
 
 func (g golang) RunLocalTest(q *leetcode.QuestionData, outDir string) (bool, error) {
-	cmd := exec.Command("go", "list", "-m")
-	cmd.Dir = outDir
-	output, err := cmd.Output()
+	genResult, err := g.GeneratePaths(q)
 	if err != nil {
-		return false, fmt.Errorf("go list failed: %w", err)
+		return false, fmt.Errorf("generate paths failed: %w", err)
 	}
-	modPath := strings.TrimSpace(string(output))
-	if modPath == "" {
-		return false, fmt.Errorf("go mod path is empty")
+	genResult.SetOutDir(outDir)
+
+	args := []string{"go", "run", "./" + genResult.SubDir}
+	return runTest(q, genResult, args, outDir)
+}
+
+// convertToGoType converts LeetCode type name to Go type name.
+func convertToGoType(typeName string) string {
+	switch typeName {
+	case "integer":
+		return "int"
+	case "long":
+		return "int64"
+	case "double":
+		return "float64"
+	case "boolean":
+		return "bool"
+	case "character":
+		return "byte"
+	case "void":
+		return ""
+	case "TreeNode":
+		return "*TreeNode"
+	case "ListNode":
+		return "*ListNode"
+	default:
+		if strings.HasSuffix(typeName, "[]") {
+			return "[]" + convertToGoType(typeName[:len(typeName)-2])
+		}
 	}
+	return typeName
+}
 
-	genResult, err := g.GeneratePaths(q)
-	if err != nil {
-		return false, fmt.Errorf("generate paths failed: %w", err)
+func (g golang) generateNormalTestCode(q *leetcode.QuestionData) (string, error) {
+	const template = `func main() {
+	stdin := bufio.NewReader(os.Stdin)
+%s
+}
+`
+	code := ""
+	paramNames := make([]string, 0, len(q.MetaData.Params))
+	for _, param := range q.MetaData.Params {
+		code += fmt.Sprintf(
+			"\t%s := Deserialize[%s](ReadLine(stdin))\n",
+			param.Name,
+			convertToGoType(param.Type),
+		)
+		paramNames = append(paramNames, param.Name)
+	}
+	if q.MetaData.Return != nil && q.MetaData.Return.Type != "void" {
+		code += fmt.Sprintf(
+			"\tans := %s(%s)\n",
+			q.MetaData.Name,
+			strings.Join(paramNames, ", "),
+		)
+	} else {
+		code += fmt.Sprintf(
+			"\t%s(%s)\n",
+			q.MetaData.Name,
+			strings.Join(paramNames, ", "),
+		)
+		ansName := paramNames[q.MetaData.Output.ParamIndex]
+		code += fmt.Sprintf("\tans := %s\n", ansName)
 	}
-	path := genResult.Files[0].Path
-	basePath := filepath.Clean(filepath.Dir(path))
+	code += fmt.Sprintf(
+		"\tfmt.Println(\"%s \" + Serialize(ans))",
+		testCaseOutputMark,
+	)
 
-	cmd = exec.Command("go", "test", "-v", modPath+"/"+basePath)
-	cmd.Dir = outDir
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	err = cmd.Run()
+	testContent := fmt.Sprintf(template, code)
+	return testContent, nil
+}
 
-	return err == nil, nil
+// nolint: staticcheck
+func toGoFuncName(f string) string {
+	return strings.Title(f)
+}
+
+func (g golang) generateSystemDesignTestCode(q *leetcode.QuestionData) (string, error) {
+	const template = `func main() {
+	stdin := bufio.NewReader(os.Stdin)
+	ops := Deserialize[[]string](ReadLine(stdin))
+	params := MustSplitArray(ReadLine(stdin))
+	output := make([]string, 0, len(ops))
+	output = append(output, "null")
+
+%s
+
+	for i := 1; i < len(ops); i++ {
+		switch ops[i] {
+%s
+		}
+	}
+	fmt.Println("%s " + JoinArray(output))
+}
+`
+	var prepareCode string
+	var paramNames []string
+	if len(q.MetaData.Constructor.Params) > 0 {
+		prepareCode += "\tconstructorParams := MustSplitArray(params[0])\n"
+		for i, param := range q.MetaData.Constructor.Params {
+			prepareCode += fmt.Sprintf(
+				"\t%s := Deserialize[%s](constructorParams[%d])\n",
+				param.Name,
+				convertToGoType(param.Type),
+				i,
+			)
+			paramNames = append(paramNames, param.Name)
+		}
+	}
+	prepareCode += fmt.Sprintf("\tobj := Constructor(%s)", strings.Join(paramNames, ", "))
+
+	callCode := ""
+	for _, method := range q.MetaData.Methods {
+		methodCall := "\t\tcase \"" + method.Name + "\":\n"
+		if len(method.Params) > 0 {
+			methodCall += "\t\t\tmethodParams := MustSplitArray(params[i])\n"
+		}
+		var methodParamNames []string
+		for i, param := range method.Params {
+			methodCall += fmt.Sprintf(
+				"\t\t\t%s := Deserialize[%s](methodParams[%d])\n",
+				param.Name,
+				convertToGoType(param.Type),
+				i,
+			)
+			methodParamNames = append(methodParamNames, param.Name)
+		}
+		if method.Return.Type != "" && method.Return.Type != "void" {
+			methodCall += fmt.Sprintf(
+				"\t\t\tans := Serialize(obj.%s(%s))\n\t\t\toutput = append(output, ans)\n",
+				toGoFuncName(method.Name),
+				strings.Join(methodParamNames, ", "),
+			)
+		} else {
+			methodCall += fmt.Sprintf(
+				"\t\t\tobj.%s(%s)\n",
+				toGoFuncName(method.Name),
+				strings.Join(methodParamNames, ", "),
+			)
+			methodCall += "\t\t\toutput = append(output, \"null\")\n"
+		}
+		callCode += methodCall
+	}
+	callCode = callCode[:len(callCode)-1] // remove last newline
+	testContent := fmt.Sprintf(
+		template,
+		prepareCode,
+		callCode,
+		testCaseOutputMark,
+	)
+	return testContent, nil
+}
+
+func (g golang) generateTestContent(q *leetcode.QuestionData) (string, error) {
+	if q.MetaData.SystemDesign {
+		return g.generateSystemDesignTestCode(q)
+	}
+	return g.generateNormalTestCode(q)
 }
 
 func (g golang) generateCodeFile(
 	q *leetcode.QuestionData,
-	baseFilename string,
+	filename string,
 	blocks []config.Block,
 	modifiers []ModifierFunc,
 	separateDescriptionFile bool,
@@ -201,9 +309,35 @@ func (g golang) generateCodeFile(
 	FileOutput,
 	error,
 ) {
+	codeHeader := fmt.Sprintf(
+		`package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+
+	. "%s"
+)`, config.GoTestUtilsModPath,
+	)
+	testContent, err := g.generateTestContent(q)
+	if err != nil {
+		return FileOutput{}, err
+	}
+	// TODO warn user that should delete global config and init again
+	blocks = append(
+		blocks,
+		config.Block{
+			Name:     internalBeforeMarker,
+			Template: codeHeader,
+		},
+		config.Block{
+			Name:     internalAfterMarker,
+			Template: testContent,
+		},
+	)
 	content, err := g.generateCodeContent(
 		q,
-		baseFilename,
 		blocks,
 		modifiers,
 		separateDescriptionFile,
@@ -212,75 +346,44 @@ func (g golang) generateCodeFile(
 		return FileOutput{}, err
 	}
 	return FileOutput{
-		Path:    filepath.Join(baseFilename, "solution.go"),
-		Content: content,
-		Type:    CodeFile,
+		Filename: filename,
+		Content:  content,
+		Type:     CodeFile | TestFile,
 	}, nil
 }
 
-func (g golang) generateTestFile(q *leetcode.QuestionData, baseFilename string, testcases string) (FileOutput, error) {
-	var funcName, testFuncName string
-	if q.MetaData.SystemDesign {
-		funcName = "Constructor"
-		testFuncName = "RunClassTestsWithString"
-	} else {
-		funcName = q.MetaData.Name
-		testFuncName = "RunTestsWithString"
-	}
-	testContent := fmt.Sprintf(
-		testFileTemplate,
-		config.ProjectURL,
-		config.GoTestUtilsModPath,
-		testcases,
-		funcName,
-		testFuncName,
-		funcName,
-	)
-	return FileOutput{
-		Path:    filepath.Join(baseFilename, "solution_test.go"),
-		Content: testContent,
-		Type:    TestFile,
-	}, nil
-}
-
-func (g golang) generateDescriptionFile(q *leetcode.QuestionData, baseFilename string) (FileOutput, error) {
-	return g.baseLang.generateDescriptionFile(q, filepath.Join(baseFilename, "question"))
-}
-
 func (g golang) GeneratePaths(q *leetcode.QuestionData) (*GenerateResult, error) {
 	filenameTmpl := getFilenameTemplate(q, g)
 	baseFilename, err := q.GetFormattedFilename(g.slug, filenameTmpl)
 	if err != nil {
 		return nil, err
 	}
-	codeFile := filepath.Join(baseFilename, "solution.go")
-	testFile := filepath.Join(baseFilename, "solution_test.go")
-
-	files := []FileOutput{
-		{
-			Path: codeFile,
-			Type: CodeFile,
+	genResult := &GenerateResult{
+		SubDir:   baseFilename,
+		Question: q,
+		Lang:     g,
+	}
+	genResult.AddFile(
+		FileOutput{
+			Filename: "solution.go",
+			Type:     CodeFile | TestFile,
 		},
-		{
-			Path: testFile,
-			Type: TestFile,
+	)
+	genResult.AddFile(
+		FileOutput{
+			Filename: "testcases.txt",
+			Type:     TestCasesFile,
 		},
-	}
-
+	)
 	if separateDescriptionFile(g) {
-		files = append(
-			files, FileOutput{
-				Path: filepath.Join(baseFilename, "question.md"),
-				Type: DocFile,
+		genResult.AddFile(
+			FileOutput{
+				Filename: "question.md",
+				Type:     DocFile,
 			},
 		)
 	}
-
-	return &GenerateResult{
-		Question: q,
-		Lang:     g,
-		Files:    files,
-	}, nil
+	return genResult, nil
 }
 
 var goBuiltinModifiers = map[string]ModifierFunc{
@@ -296,6 +399,11 @@ func (g golang) Generate(q *leetcode.QuestionData) (*GenerateResult, error) {
 	if err != nil {
 		return nil, err
 	}
+	genResult := &GenerateResult{
+		Question: q,
+		Lang:     g,
+		SubDir:   baseFilename,
+	}
 
 	separateDescriptionFile := separateDescriptionFile(g)
 	blocks := getBlocks(g)
@@ -303,29 +411,24 @@ func (g golang) Generate(q *leetcode.QuestionData) (*GenerateResult, error) {
 	if err != nil {
 		return nil, err
 	}
-	codeFile, err := g.generateCodeFile(q, baseFilename, blocks, modifiers, separateDescriptionFile)
+	codeFile, err := g.generateCodeFile(q, "solution.go", blocks, modifiers, separateDescriptionFile)
 	if err != nil {
 		return nil, err
 	}
-
-	testcaseStr := g.generateTestCases(q)
-	testFile, err := g.generateTestFile(q, baseFilename, testcaseStr)
+	testcaseFile, err := g.generateTestCasesFile(q, "testcases.txt")
 	if err != nil {
 		return nil, err
 	}
-	files := []FileOutput{codeFile, testFile}
+	genResult.AddFile(codeFile)
+	genResult.AddFile(testcaseFile)
 
 	if separateDescriptionFile {
-		docFile, err := g.generateDescriptionFile(q, baseFilename)
+		docFile, err := g.generateDescriptionFile(q, "question.md")
 		if err != nil {
 			return nil, err
 		}
-		files = append(files, docFile)
+		genResult.AddFile(docFile)
 	}
 
-	return &GenerateResult{
-		Question: q,
-		Lang:     g,
-		Files:    files,
-	}, nil
+	return genResult, nil
 }
diff --git a/lang/judge.go b/lang/judge.go
new file mode 100644
index 00000000..88a3b40c
--- /dev/null
+++ b/lang/judge.go
@@ -0,0 +1,10 @@
+package lang
+
+import (
+	"github.com/j178/leetgo/leetcode"
+)
+
+func judgeResult(q *leetcode.QuestionData, actual, expected string) bool {
+	// TODO compare by question rules
+	return actual == expected
+}
diff --git a/lang/tmpl.go b/lang/tmpl.go
deleted file mode 100644
index 56a92e20..00000000
--- a/lang/tmpl.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package lang
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/charmbracelet/log"
-	"github.com/dop251/goja"
-	"github.com/spf13/viper"
-
-	"github.com/j178/leetgo/config"
-	"github.com/j178/leetgo/leetcode"
-)
-
-const (
-	defaultContentTemplate = `
-{{- block "header" . -}}
-{{ .LineComment }} Created by {{ .Author }} at {{ .Time }}
-{{ .LineComment }} {{ .Question.Url }}
-{{ if .Question.IsContest }}{{ .LineComment }} {{ .Question.ContestUrl }}
-{{ end }}
-{{ end }}
-{{ block "description" . -}}
-{{ .BlockCommentStart }}
-{{ block "title" . }}{{ .Question.QuestionFrontendId }}. {{ .Question.GetTitle }} ({{ .Question.Difficulty }}){{ end }}
-{{ .Question.GetFormattedContent }}
-{{ .BlockCommentEnd }}
-{{ end }}
-{{ block "beforeMarker" . }}{{ end }}
-{{ .LineComment }} {{ .CodeBeginMarker }}
-{{ block "beforeCode" . }}{{ end }}
-{{ block "code" . }}{{ .Code | runModifiers }}{{ end }}
-{{ block "afterCode" . }}{{ end }}
-{{ .LineComment }} {{ .CodeEndMarker }}
-{{ block "afterMarker" . }}{{ end }}
-`
-
-	withoutDescriptionContentTemplate = `
-{{- block "header" . -}}
-{{ .LineComment }} Created by {{ .Author }} at {{ .Time }}
-{{ .LineComment }} {{ .Question.Url }}
-{{ if .Question.IsContest }}{{ .LineComment }} {{ .Question.ContestUrl }}
-{{ end }}
-{{ end }}
-{{ block "beforeMarker" . }}{{ end }}
-{{ .LineComment }} {{ .CodeBeginMarker }}
-{{ block "beforeCode" . }}{{ end }}
-{{ block "code" . }}{{ .Code | runModifiers }}{{ end }}
-{{ block "afterCode" . }}{{ end }}
-{{ .LineComment }} {{ .CodeEndMarker }}
-{{ block "afterMarker" . }}{{ end }}
-`
-)
-
-type contentData struct {
-	Question          *leetcode.QuestionData
-	Author            string
-	Time              string
-	LineComment       string
-	BlockCommentStart string
-	BlockCommentEnd   string
-	CodeBeginMarker   string
-	CodeEndMarker     string
-	Code              string
-	NeedsDefinition   bool
-}
-
-var validBlocks = map[string]bool{
-	"header":       true,
-	"description":  true,
-	"title":        true,
-	"beforeMarker": true,
-	"beforeCode":   true,
-	"code":         true,
-	"afterCode":    true,
-	"afterMarker":  true,
-}
-
-var builtinModifiers = map[string]ModifierFunc{
-	"removeUselessComments": removeUselessComments,
-}
-
-type ModifierFunc func(string, *leetcode.QuestionData) string
-
-func getBlocks(lang Lang) (ans []config.Block) {
-	blocks := viper.Get("code." + lang.Slug() + ".blocks")
-	if blocks == nil || len(blocks.([]any)) == 0 {
-		blocks = viper.Get("code." + lang.ShortName() + ".blocks")
-	}
-	if blocks == nil || len(blocks.([]any)) == 0 {
-		blocks = viper.Get("code.blocks")
-	}
-	if blocks == nil {
-		return
-	}
-	for _, b := range blocks.([]any) {
-		ans = append(
-			ans, config.Block{
-				Name:     b.(map[string]any)["name"].(string),
-				Template: b.(map[string]any)["template"].(string),
-			},
-		)
-	}
-	return
-}
-
-func getModifiers(lang Lang, modifiersMap map[string]ModifierFunc) ([]ModifierFunc, error) {
-	modifiers := viper.Get("code." + lang.Slug() + ".modifiers")
-	if modifiers == nil || len(modifiers.([]any)) == 0 {
-		modifiers = viper.Get("code." + lang.ShortName() + ".modifiers")
-	}
-	if modifiers == nil || len(modifiers.([]any)) == 0 {
-		modifiers = viper.Get("code.modifiers")
-	}
-	if modifiers == nil {
-		return nil, nil
-	}
-
-	var funcs []ModifierFunc
-	for _, m := range modifiers.([]any) {
-		m := m.(map[string]any)
-		name, script := "", ""
-
-		if m["name"] != nil {
-			name = m["name"].(string)
-			if f, ok := modifiersMap[name]; ok {
-				funcs = append(funcs, f)
-				continue
-			}
-		}
-		if m["script"] != nil {
-			script = m["script"].(string)
-			vm := goja.New()
-			_, err := vm.RunString(script)
-			if err != nil {
-				return nil, fmt.Errorf("failed to run script: %w", err)
-			}
-			var jsFn func(string) string
-			if vm.Get("modify") == nil {
-				return nil, fmt.Errorf("failed to get modify function")
-			}
-			err = vm.ExportTo(vm.Get("modify"), &jsFn)
-			if err != nil {
-				return nil, fmt.Errorf("failed to export function: %w", err)
-			}
-			f := func(s string, data *leetcode.QuestionData) string {
-				return jsFn(s)
-			}
-			funcs = append(funcs, f)
-			continue
-		}
-		log.Warn("invalid modifier, ignored", "name", name, "script", script)
-	}
-	return funcs, nil
-}
-
-func needsDefinition(code string) bool {
-	return strings.Contains(code, "Definition for")
-}
-
-func needsMod(content string) bool {
-	return strings.Contains(content, "<code>10<sup>9</sup> + 7</code>") || strings.Contains(content, "10^9 + 7")
-}
-
-func removeUselessComments(code string, q *leetcode.QuestionData) string {
-	lines := strings.Split(code, "\n")
-	var newLines []string
-	for i := 0; i < len(lines); i++ {
-		line := lines[i]
-		if strings.HasPrefix(line, "/**") && (strings.Contains(
-			lines[i+1],
-			"object will be instantiated and called",
-		) || strings.Contains(lines[i+1], "Definition for")) {
-			for {
-				i++
-				if strings.HasSuffix(lines[i], "*/") {
-					break
-				}
-			}
-			continue
-		}
-		newLines = append(newLines, line)
-	}
-	return strings.Join(newLines, "\n")
-}
diff --git a/leetcode/models.go b/leetcode/models.go
index e078fb58..94010862 100644
--- a/leetcode/models.go
+++ b/leetcode/models.go
@@ -78,6 +78,7 @@ type SubmitCheckResult struct {
 }
 
 var (
+	// TODO replace with lipgloss
 	colorGreen  = color.New(color.FgHiGreen, color.Bold)
 	colorYellow = color.New(color.FgHiYellow, color.Bold)
 	colorFaint  = color.New(color.Faint)
diff --git a/leetcode/question.go b/leetcode/question.go
index 56e257ba..bf5e3c08 100644
--- a/leetcode/question.go
+++ b/leetcode/question.go
@@ -61,9 +61,10 @@ type MetaDataParam struct {
 }
 
 type MetaDataReturn struct {
-	Type    string `json:"type"`
-	Size    int    `json:"size"`
-	Dealloc bool   `json:"dealloc"`
+	Type string `json:"type"`
+	// Size    *int   `json:"size"`
+	// ColSize *int `json:"colsize"`
+	Dealloc bool `json:"dealloc"`
 }
 
 type MetaDataOutput struct {
@@ -151,8 +152,8 @@ type MetaDataConstructor struct {
 type MetaData struct {
 	Name   string          `json:"name"`
 	Params []MetaDataParam `json:"params"`
-	Return MetaDataReturn  `json:"return"`
-	Output MetaDataOutput  `json:"output"`
+	Return *MetaDataReturn `json:"return"`
+	Output *MetaDataOutput `json:"output"`
 	// System design problems related
 	SystemDesign bool                `json:"systemdesign"`
 	ClassName    string              `json:"classname"`
@@ -164,6 +165,34 @@ type MetaData struct {
 
 type metaDataNoMethods MetaData
 
+// Type name in metadata is not consistent, we need to normalize it.
+func normalizeType(ty string) string {
+	switch {
+	case strings.HasPrefix(ty, "list<"):
+		return normalizeType(ty[5:len(ty)-1]) + "[]" // "list<int>" -> "int[]"
+	case ty == "String":
+		return "string"
+	case ty == "":
+		return "void"
+	}
+	return ty
+}
+
+func (m *MetaData) normalize() {
+	for i, param := range m.Params {
+		m.Params[i].Type = normalizeType(param.Type)
+	}
+	if m.Return != nil {
+		m.Return.Type = normalizeType(m.Return.Type)
+	}
+	for _, method := range m.Methods {
+		for i, param := range method.Params {
+			method.Params[i].Type = normalizeType(param.Type)
+		}
+		method.Return.Type = normalizeType(method.Return.Type)
+	}
+}
+
 func (m *MetaData) UnmarshalJSON(data []byte) error {
 	// Ignore error, when we load from sqlite, no need to unquote it.
 	unquoted, err := strconv.Unquote(utils.BytesToString(data))
@@ -174,9 +203,25 @@ func (m *MetaData) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
+	m.normalize()
 	return nil
 }
 
+func (m *MetaData) NArg() int {
+	if m.SystemDesign {
+		return 2
+	}
+	return len(m.Params)
+}
+
+func (m *MetaData) ResultType() string {
+	if m.Return != nil && m.Return.Type != "void" {
+		return m.Return.Type
+	} else {
+		return m.Params[m.Output.ParamIndex].Type
+	}
+}
+
 type JsonExampleTestCases []string
 
 type jsonExampleTestCasesNoMethods JsonExampleTestCases


Test patch:
diff --git a/cmd/contest.go b/cmd/contest.go
index 704fe548..94c2baf6 100644
--- a/cmd/contest.go
+++ b/cmd/contest.go
@@ -199,7 +199,7 @@ leetgo contest left w330
 				_ = browser.OpenURL(r.Question.ContestUrl())
 			}
 		}
-		err = editor.Open(generated[0].Files)
+		err = editor.Open(generated[0])
 		return err
 	},
 }
diff --git a/cmd/test.go b/cmd/test.go
index 23e15f2c..c090f0a7 100644
--- a/cmd/test.go
+++ b/cmd/test.go
@@ -39,7 +39,7 @@ func init() {
 		false,
 		"run test both locally and remotely",
 	)
-	testCmd.Flags().StringSliceVarP(&customCases, "cases", "c", nil, "custom test cases")
+	testCmd.Flags().StringSliceVarP(&customCases, "cases", "c", nil, "additional test cases for remote test")
 	testCmd.Flags().BoolVarP(&autoSubmit, "submit", "s", false, "auto submit if all tests passed")
 }
 
diff --git a/lang/test.go b/lang/test.go
new file mode 100644
index 00000000..41bb5939
--- /dev/null
+++ b/lang/test.go
@@ -0,0 +1,363 @@
+package lang
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"os/exec"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/charmbracelet/lipgloss"
+	"github.com/j178/leetgo/config"
+	"github.com/j178/leetgo/leetcode"
+	goutils "github.com/j178/leetgo/testutils/go"
+	"github.com/j178/leetgo/utils"
+	"github.com/jedib0t/go-pretty/v6/list"
+)
+
+func RunLocalTest(q *leetcode.QuestionData) (bool, error) {
+	cfg := config.Get()
+	gen, err := GetGenerator(cfg.Code.Lang)
+	if err != nil {
+		return false, err
+	}
+	tester, ok := gen.(LocalTestable)
+	if !ok {
+		return false, fmt.Errorf("language %s does not support local test", gen.Slug())
+	}
+	err = q.Fulfill()
+	if err != nil {
+		return false, fmt.Errorf("failed to get question data: %w", err)
+	}
+	outDir := getOutDir(q, gen)
+	if !utils.IsExist(outDir) {
+		return false, fmt.Errorf("no code generated for %s in language %s", q.TitleSlug, gen.Slug())
+	}
+
+	return tester.RunLocalTest(q, outDir)
+}
+
+// typeNameToType converts a Go type name to reflect.Type.
+func typeNameToType(ty string) reflect.Type {
+	switch ty {
+	case "int":
+		return reflect.TypeOf(0)
+	case "int64":
+		return reflect.TypeOf(int64(0))
+	case "float64":
+		return reflect.TypeOf(float64(0))
+	case "string":
+		return reflect.TypeOf("")
+	case "bool":
+		return reflect.TypeOf(false)
+	case "byte":
+		return reflect.TypeOf(byte(0))
+	case "*TreeNode":
+		return reflect.TypeOf((*goutils.TreeNode)(nil))
+	case "*ListNode":
+		return reflect.TypeOf((*goutils.ListNode)(nil))
+	default:
+		if strings.HasPrefix(ty, "[]") {
+			et := typeNameToType(ty[2:])
+			if et == nil {
+				return nil
+			}
+			return reflect.SliceOf(et)
+		}
+	}
+	return nil
+}
+
+func deserialize(tpName string, raw string) (reflect.Value, error) {
+	raw = strings.TrimSpace(raw)
+	goTpName := convertToGoType(tpName)
+	ty := typeNameToType(goTpName)
+	if ty == nil {
+		return reflect.Value{}, fmt.Errorf("unknown type: %s", tpName)
+	}
+	return goutils.DeserializeValue(ty, raw)
+}
+
+type testCase struct {
+	no     int
+	input  []string
+	output string
+}
+
+func (c testCase) Input() string {
+	return utils.EnsureTrailingNewline(strings.Join(c.input, "\n"))
+}
+
+type testCases struct {
+	cases      []testCase
+	targetCase int
+}
+
+func checkTestCases(q *leetcode.QuestionData, tc testCases) error {
+	narg := q.MetaData.NArg()
+	if q.MetaData.SystemDesign {
+		// System design questions have two inputs, the first one is a list of strings, but the second is a list of
+		// different types. We just check if it's a valid list.
+		// input:
+		// ["LRUCache","put","put","get","put","get","put","get","get","get"]
+		// [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
+		// output:
+		// [null,null,null,1,null,-1,null,-1,3,4]
+		for _, c := range tc.cases {
+			if len(c.input) != narg {
+				return fmt.Errorf("should have %d arguments, got %d", narg, len(c.input))
+			}
+			if _, err := deserialize("[]string", c.input[0]); err != nil {
+				return fmt.Errorf("cannot parse %s as []string", c.input[0])
+			}
+			if _, err := goutils.SplitArray(c.input[1]); err != nil {
+				return fmt.Errorf("%s is not a valid list", c.input[0])
+			}
+			if _, err := goutils.SplitArray(c.output); err != nil {
+				return fmt.Errorf("%s is not a valid list", c.input[0])
+			}
+		}
+		return nil
+	}
+
+	resultType := q.MetaData.ResultType()
+	for _, c := range tc.cases {
+		if len(c.input) != narg {
+			return fmt.Errorf("should have %d arguments, got %d", narg, len(c.input))
+		}
+		for j, arg := range c.input {
+			tp := q.MetaData.Params[j].Type
+			if _, err := deserialize(tp, arg); err != nil {
+				return fmt.Errorf("cannot parse %s as %s", arg, tp)
+			}
+		}
+		if _, err := deserialize(resultType, c.output); err != nil {
+			return fmt.Errorf("cannot parse %s as %s", c.output, resultType)
+		}
+	}
+	return nil
+}
+
+func parseTestCases(q *leetcode.QuestionData, f *FileOutput) (testCases, error) {
+	tc := testCases{}
+	content, err := f.GetContent()
+	if err != nil {
+		return tc, err
+	}
+	var (
+		inputLines    []string
+		output        string
+		inputStarted  bool
+		outputStarted bool
+	)
+	lines := strings.Split(content, "\n")
+	for _, line := range lines {
+		line := strings.TrimSpace(line)
+		switch {
+		case line == "":
+			continue
+		case strings.HasPrefix(line, testCaseTargetMark):
+			no := strings.TrimSpace(line[len(testCaseTargetMark):])
+			targetCase, err := strconv.Atoi(no)
+			if err != nil {
+				return tc, fmt.Errorf("invalid target_case: %s is not valid number", no)
+			}
+			tc.targetCase = targetCase
+		case strings.HasPrefix(line, testCaseInputMark):
+			inputStarted = true
+			outputStarted = false
+			if len(inputLines) > 0 && len(output) > 0 {
+				tc.cases = append(
+					tc.cases, testCase{
+						no:     len(tc.cases) + 1,
+						input:  append([]string(nil), inputLines...),
+						output: output,
+					},
+				)
+				inputLines = inputLines[:0]
+				output = ""
+			}
+		case strings.HasPrefix(line, testCaseOutputMark):
+			outputStarted = true
+			inputStarted = false
+		case inputStarted:
+			inputLines = append(inputLines, line)
+		case outputStarted:
+			if len(output) > 0 {
+				return tc, errors.New("invalid test case: output should be a single line")
+			}
+			output = line
+		}
+	}
+	if len(inputLines) > 0 && len(output) > 0 {
+		tc.cases = append(
+			tc.cases, testCase{
+				no:     len(tc.cases) + 1,
+				input:  append([]string(nil), inputLines...),
+				output: output,
+			},
+		)
+	}
+	if tc.targetCase > len(tc.cases) {
+		return tc, fmt.Errorf("invalid target_case: %d, maximum is %d", tc.targetCase, len(tc.cases))
+	}
+	if tc.targetCase < 0 {
+		tc.targetCase += len(tc.cases) + 1
+	}
+
+	if err := checkTestCases(q, tc); err != nil {
+		return tc, fmt.Errorf("invalid test case: %w", err)
+	}
+
+	return tc, nil
+}
+
+func extractOutput(s string) (string, string) {
+	var output string
+	var others []string
+	for _, line := range strings.Split(s, "\n") {
+		if strings.HasPrefix(line, testCaseOutputMark) {
+			// If there are multiple output lines, only the last one is used.
+			output = strings.TrimSpace(line[len(testCaseOutputMark):])
+		} else {
+			others = append(others, line)
+		}
+	}
+	return output, strings.Join(others, "\n")
+}
+
+func checkOutput(q *leetcode.QuestionData, outputLine string) error {
+	if outputLine == "" {
+		return fmt.Errorf("no output found")
+	}
+	if q.MetaData.SystemDesign {
+		_, err := goutils.SplitArray(outputLine)
+		if err != nil {
+			return fmt.Errorf("invalid output: %s", outputLine)
+		}
+		return nil
+	}
+	tp := q.MetaData.ResultType()
+	_, err := deserialize(tp, outputLine)
+	if err != nil {
+		return fmt.Errorf("invalid output: %s", outputLine)
+	}
+	return nil
+}
+
+var (
+	skippedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#b8b8b8"))
+	passedStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#00b300"))
+	errorStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000"))
+	failedStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff6600"))
+	stdoutStyle  = lipgloss.NewStyle().Faint(true)
+)
+
+func runTest(q *leetcode.QuestionData, genResult *GenerateResult, args []string, outDir string) (bool, error) {
+	testcaseFile := genResult.GetFile(TestCasesFile)
+	if testcaseFile == nil {
+		panic("no test cases file generated")
+	}
+	tc, err := parseTestCases(q, testcaseFile)
+	if err != nil {
+		return false, err
+	}
+	if len(tc.cases) == 0 {
+		return false, fmt.Errorf("no test cases found")
+	}
+	var (
+		outputBuf bytes.Buffer
+		ran       int
+		passed    int
+	)
+	for _, c := range tc.cases {
+		func() {
+			l := list.NewWriter()
+			l.SetStyle(list.StyleBulletCircle)
+			defer func() {
+				fmt.Println(l.Render())
+			}()
+			if tc.targetCase != 0 && c.no != tc.targetCase {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, skippedStyle.Render("Skipped")))
+				return
+			}
+			ran++
+			outputBuf.Reset()
+			ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+			defer cancel()
+
+			cmd := exec.CommandContext(ctx, args[0], args[1:]...)
+			cmd.Dir = outDir
+			cmd.Stdin = strings.NewReader(c.Input())
+			cmd.Stdout = &outputBuf
+			cmd.Stderr = &outputBuf
+			err = cmd.Start()
+			if err != nil {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, errorStyle.Render("Failed to start")))
+				return
+			}
+			done := make(chan error, 1)
+			go func() {
+				done <- cmd.Wait()
+			}()
+			select {
+			case <-ctx.Done():
+			case err = <-done:
+			}
+
+			actualOutput, stdout := extractOutput(outputBuf.String())
+			mayAppendStdout := func() {
+				if stdout != "" {
+					l.AppendItem(fmt.Sprintf("Stdout:     %s", stdoutStyle.Render(stdout)))
+				}
+			}
+			if ctx.Err() != nil {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, errorStyle.Render("Time limit exceeded")))
+				l.Indent()
+				l.AppendItem(fmt.Sprintf("Input:      %s", strings.ReplaceAll(c.Input(), "\n", "↩ ")))
+				mayAppendStdout()
+				l.UnIndent()
+				return
+			}
+			if err != nil {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, errorStyle.Render("Runtime error")))
+				l.Indent()
+				l.AppendItem(fmt.Sprintf("Input:      %s", strings.ReplaceAll(c.Input(), "\n", "↩ ")))
+				mayAppendStdout()
+				l.UnIndent()
+				return
+			}
+			err = checkOutput(q, actualOutput)
+			if err != nil {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, errorStyle.Render("Invalid output")))
+				l.Indent()
+				l.AppendItem(fmt.Sprintf("Input:      %s", strings.ReplaceAll(c.Input(), "\n", "↩ ")))
+				l.AppendItem(fmt.Sprintf("Output:     %s", actualOutput))
+				mayAppendStdout()
+				l.UnIndent()
+				return
+			}
+
+			if judgeResult(q, actualOutput, c.output) {
+				passed++
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, passedStyle.Render("Accepted")))
+			} else {
+				l.AppendItem(fmt.Sprintf("Case %d:    %s", c.no, failedStyle.Render("Wrong answer")))
+				l.Indent()
+				l.AppendItem(fmt.Sprintf("Input:      %s", strings.ReplaceAll(c.Input(), "\n", "↩ ")))
+				l.AppendItem(fmt.Sprintf("Output:     %s", actualOutput))
+				l.AppendItem(fmt.Sprintf("Expected:   %s", c.output))
+				mayAppendStdout()
+				l.UnIndent()
+			}
+		}()
+	}
+	if passed == ran {
+		return true, nil
+	}
+	return false, nil
+}
diff --git a/testutils/go/config.go b/testutils/go/config.go
deleted file mode 100644
index 22b0fbcb..00000000
--- a/testutils/go/config.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package common
-
-import "time"
-
-var (
-    // 用于编写 struct + method 的题目，方便打断点
-    DebugCallIndex int
-
-    DebugTLE = 2 * time.Second
-
-    AssertOutput = true
-)
diff --git a/testutils/go/parse.go b/testutils/go/parse.go
new file mode 100644
index 00000000..08bd3b71
--- /dev/null
+++ b/testutils/go/parse.go
@@ -0,0 +1,202 @@
+package goutils
+
+import (
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+func MustSplitArray(raw string) []string {
+	raw = strings.TrimSpace(raw)
+	splits, err := SplitArray(raw)
+	if err != nil {
+		panic(err)
+	}
+	return splits
+}
+
+func SplitArray(raw string) (splits []string, err error) {
+	invalidErr := fmt.Errorf("invalid array: %s", raw)
+
+	// check [] at leftmost and rightmost
+	if len(raw) <= 1 || raw[0] != '[' || raw[len(raw)-1] != ']' {
+		return nil, invalidErr
+	}
+
+	// ignore [] at leftmost and rightmost
+	raw = raw[1 : len(raw)-1]
+	if raw == "" {
+		return
+	}
+
+	var depth, quote int
+	for i := 0; i < len(raw); {
+		j := i
+	outer:
+		for ; j < len(raw); j++ {
+			switch raw[j] {
+			case '[':
+				depth++
+			case ']':
+				depth--
+			case '"':
+				quote++
+			case ',':
+				if depth == 0 && quote%2 == 0 {
+					break outer
+				}
+			}
+		}
+		splits = append(splits, strings.TrimSpace(raw[i:j]))
+		i = j + 1 // skip sep
+	}
+	if depth != 0 || quote%2 != 0 {
+		return nil, invalidErr
+	}
+	return
+}
+
+// DeserializeValue deserialize a string to a reflect.Value
+func DeserializeValue(ty reflect.Type, raw string) (reflect.Value, error) {
+	z := reflect.Value{}
+	switch ty.Kind() {
+	case reflect.Bool:
+		if raw != "true" && raw != "false" {
+			return z, fmt.Errorf("invalid bool: %s", raw)
+		}
+		b := raw == "true"
+		return reflect.ValueOf(b), nil
+	case reflect.Uint8: // byte
+		if len(raw) != 3 || raw[0] != '"' && raw[0] != '\'' || raw[2] != raw[0] {
+			return z, fmt.Errorf("invalid byte: %s", raw)
+		}
+		return reflect.ValueOf(raw[1]), nil
+	case reflect.String:
+		s, err := strconv.Unquote(raw)
+		if err != nil {
+			return z, fmt.Errorf("invalid string: %s", raw)
+		}
+		return reflect.ValueOf(s), nil
+	case reflect.Int, reflect.Int32:
+		i, err := strconv.Atoi(raw)
+		if err != nil {
+			return z, fmt.Errorf("invalid int: %s", raw)
+		}
+		return reflect.ValueOf(i), nil
+	case reflect.Int64:
+		i, err := strconv.ParseInt(raw, 10, 64)
+		if err != nil {
+			return z, fmt.Errorf("invalid int64: %s", raw)
+		}
+		return reflect.ValueOf(i), nil
+	case reflect.Uint, reflect.Uint32:
+		i, err := strconv.ParseUint(raw, 10, 32)
+		if err != nil {
+			return z, fmt.Errorf("invalid uint: %s", raw)
+		}
+		return reflect.ValueOf(uint(i)), nil
+	case reflect.Uint64:
+		i, err := strconv.ParseUint(raw, 10, 64)
+		if err != nil {
+			return z, fmt.Errorf("invalid uint64: %s", raw)
+		}
+		return reflect.ValueOf(i), nil
+	case reflect.Float64:
+		f, err := strconv.ParseFloat(raw, 64)
+		if err != nil {
+			return z, fmt.Errorf("invalid float64: %s", raw)
+		}
+		return reflect.ValueOf(f), nil
+	case reflect.Slice:
+		splits, err := SplitArray(raw)
+		if err != nil {
+			return z, fmt.Errorf("invalid array: %s", raw)
+		}
+		sl := reflect.MakeSlice(ty, 0, len(splits))
+		for _, s := range splits {
+			e, err := DeserializeValue(ty.Elem(), s)
+			if err != nil {
+				return z, err
+			}
+			sl = reflect.Append(sl, e)
+		}
+		return sl, nil
+	case reflect.Ptr:
+		switch ty.Elem().Name() {
+		case "TreeNode":
+			root, err := DeserializeTreeNode(raw)
+			if err != nil {
+				return z, err
+			}
+			return reflect.ValueOf(root), nil
+		case "ListNode":
+			head, err := DeserializeListNode(raw)
+			if err != nil {
+				return z, err
+			}
+			return reflect.ValueOf(head), nil
+		}
+	}
+	return z, fmt.Errorf("unknown type %s", ty.String())
+}
+
+// Deserialize deserialize a string to a type.
+func Deserialize[T any](raw string) T {
+	raw = strings.TrimSpace(raw)
+	var z T
+	ty := reflect.TypeOf(z)
+	v, err := DeserializeValue(ty, raw)
+	if err != nil {
+		panic(fmt.Errorf("deserialize failed: %w", err))
+	}
+	rv := reflect.ValueOf(&z)
+	rv.Elem().Set(v)
+	return z
+}
+
+func serialize(v reflect.Value) (string, error) {
+	switch v.Kind() {
+	case reflect.Slice:
+		sb := &strings.Builder{}
+		sb.WriteByte('[')
+		for i := 0; i < v.Len(); i++ {
+			if i > 0 {
+				sb.WriteByte(',')
+			}
+			_s, err := serialize(v.Index(i))
+			if err != nil {
+				return "", err
+			}
+			sb.WriteString(_s)
+		}
+		sb.WriteByte(']')
+		return sb.String(), nil
+	case reflect.Ptr: // *TreeNode, *ListNode
+		switch tpName := v.Type().Elem().Name(); tpName {
+		case "TreeNode":
+			return v.Interface().(*TreeNode).ToString(), nil
+		case "ListNode":
+			return v.Interface().(*ListNode).ToString(), nil
+		default:
+			return "", fmt.Errorf("unknown type %s", tpName)
+		}
+	case reflect.String:
+		return fmt.Sprintf(`"%s"`, v.Interface()), nil
+	case reflect.Uint8: // byte
+		return fmt.Sprintf(`"%c"`, v.Interface()), nil
+	case reflect.Float64:
+		return fmt.Sprintf(`%.5f`, v.Interface()), nil
+	default: // int uint int64 uint64 bool
+		return fmt.Sprintf(`%v`, v.Interface()), nil
+	}
+}
+
+func Serialize(v any) string {
+	vt := reflect.ValueOf(v)
+	s, err := serialize(vt)
+	if err != nil {
+		panic(fmt.Errorf("serialize failed: %w", err))
+	}
+	return s
+}
diff --git a/testutils/go/parse_test.go b/testutils/go/parse_test.go
new file mode 100644
index 00000000..4129433f
--- /dev/null
+++ b/testutils/go/parse_test.go
@@ -0,0 +1,158 @@
+package goutils
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDeserialize(t *testing.T) {
+	v1, err := Deserialize[int]("123")
+	assert.NoError(t, err)
+	assert.Equal(t, 123, v1)
+
+	v2, err := Deserialize[string](`"abc"`)
+	assert.NoError(t, err)
+	assert.Equal(t, "abc", v2)
+
+	v3, err := Deserialize[byte](`'a'`)
+	assert.NoError(t, err)
+	assert.Equal(t, byte('a'), v3)
+
+	v4, err := Deserialize[[]int]("[]")
+	assert.NoError(t, err)
+	assert.Equal(t, []int{}, v4)
+
+	v5, err := Deserialize[[]int]("[1,2,3]")
+	assert.NoError(t, err)
+	assert.Equal(t, []int{1, 2, 3}, v5)
+
+	v6, err := Deserialize[[]string](`["a","b","c"]`)
+	assert.NoError(t, err)
+	assert.Equal(t, []string{"a", "b", "c"}, v6)
+
+	v7, err := Deserialize[*TreeNode]("[1,2,3]")
+	assert.NoError(t, err)
+	assert.Equal(t, v7.Val, 1)
+	assert.Equal(t, v7.Left.Val, 2)
+	assert.Equal(t, v7.Right.Val, 3)
+
+	v8, err := Deserialize[*ListNode]("[1,2,3]")
+	assert.NoError(t, err)
+	assert.Equal(t, v8.Val, 1)
+	assert.Equal(t, v8.Next.Val, 2)
+	assert.Equal(t, v8.Next.Next.Val, 3)
+
+	v9, err := Deserialize[float64]("1.2")
+	assert.NoError(t, err)
+	assert.Equal(t, 1.2, v9)
+
+	v10, err := Deserialize[bool]("true")
+	assert.NoError(t, err)
+	assert.Equal(t, true, v10)
+
+	v11, err := Deserialize[bool]("false")
+	assert.NoError(t, err)
+	assert.Equal(t, false, v11)
+
+	v12, err := Deserialize[[][]int]("[[1,2],[3,4]]")
+	assert.NoError(t, err)
+	assert.Equal(t, [][]int{{1, 2}, {3, 4}}, v12)
+
+	v13, err := Deserialize[[]*TreeNode]("[[1,2,3],[4,5,6]]")
+	assert.NoError(t, err)
+	assert.Len(t, v13, 2)
+
+	_, err = Deserialize[bool]("True")
+	assert.Error(t, err)
+
+	_, err = Deserialize[func()]("")
+	assert.Error(t, err)
+
+	_, err = Deserialize[int](`"1.2"`)
+	assert.Error(t, err)
+}
+
+func TestDeserializeByGoType(t *testing.T) {
+	v1, err := DeserializeByGoType("int", "123")
+	assert.NoError(t, err)
+	assert.EqualValues(t, 123, v1.Int())
+
+	v2, err := DeserializeByGoType("string", `"abc"`)
+	assert.NoError(t, err)
+	assert.Equal(t, "abc", v2.String())
+
+	v3, err := DeserializeByGoType("byte", `'a'`)
+	assert.NoError(t, err)
+	assert.Equal(t, uint8('a'), uint8(v3.Uint()))
+
+	v4, err := DeserializeByGoType("[]int", "[]")
+	assert.NoError(t, err)
+	assert.Equal(t, reflect.Slice, v4.Type().Kind())
+	assert.Equal(t, reflect.Int, v4.Type().Elem().Kind())
+	var v4Slice []int
+	reflect.ValueOf(&v4Slice).Elem().Set(v4)
+	assert.Equal(t, []int{}, v4Slice)
+
+	v5, err := DeserializeByGoType("[]int", "[1,2,3]")
+	assert.NoError(t, err)
+	assert.Equal(t, reflect.Slice, v4.Type().Kind())
+	assert.Equal(t, reflect.Int, v4.Type().Elem().Kind())
+	var v5Slice []int
+	reflect.ValueOf(&v5Slice).Elem().Set(v5)
+	assert.Equal(t, []int{1, 2, 3}, v5Slice)
+
+	v6, err := DeserializeByGoType("[]string", `["a","b","c"]`)
+	assert.NoError(t, err)
+	var v6Slice []string
+	reflect.ValueOf(&v6Slice).Elem().Set(v6)
+	assert.Equal(t, []string{"a", "b", "c"}, v6Slice)
+
+	v7, err := DeserializeByGoType("*TreeNode", "[1,2,3]")
+	assert.NoError(t, err)
+	var v7Node *TreeNode
+	reflect.ValueOf(&v7Node).Elem().Set(v7)
+	assert.Equal(t, v7Node.Val, 1)
+	assert.Equal(t, v7Node.Left.Val, 2)
+	assert.Equal(t, v7Node.Right.Val, 3)
+
+	v8, err := DeserializeByGoType("*ListNode", "[1,2,3]")
+	assert.NoError(t, err)
+	var v8Node *ListNode
+	reflect.ValueOf(&v8Node).Elem().Set(v8)
+	assert.Equal(t, v8Node.Val, 1)
+	assert.Equal(t, v8Node.Next.Val, 2)
+	assert.Equal(t, v8Node.Next.Next.Val, 3)
+
+	v9, err := DeserializeByGoType("float64", "1.2")
+	assert.NoError(t, err)
+	assert.Equal(t, float64(1.2), v9.Float())
+
+	v10, err := DeserializeByGoType("bool", "true")
+	assert.NoError(t, err)
+	assert.Equal(t, true, v10.Bool())
+
+	v11, err := DeserializeByGoType("bool", "false")
+	assert.NoError(t, err)
+	assert.Equal(t, false, v11.Bool())
+
+	v12, err := DeserializeByGoType("[][]int", "[[1,2],[3,4]]")
+	assert.NoError(t, err)
+	var v12Slice [][]int
+	reflect.ValueOf(&v12Slice).Elem().Set(v12)
+	assert.Equal(t, [][]int{{1, 2}, {3, 4}}, v12Slice)
+
+	v13, err := DeserializeByGoType("[]*TreeNode", "[[1,2,3],[4,5,6]]")
+	assert.NoError(t, err)
+	assert.Equal(t, v13.Len(), 2)
+
+	_, err = DeserializeByGoType("bool", "True")
+	assert.Error(t, err)
+
+	_, err = DeserializeByGoType("func()", "")
+	assert.Error(t, err)
+
+	_, err = DeserializeByGoType("int", `"1.2"`)
+	assert.Error(t, err)
+}
diff --git a/testutils/go/predefined.go b/testutils/go/predefined.go
index 0d6ed897..333b3cc8 100644
--- a/testutils/go/predefined.go
+++ b/testutils/go/predefined.go
@@ -1,207 +1,211 @@
-package common
+package goutils
 
 import (
-    "encoding/json"
-    "strconv"
-    "strings"
+	"encoding/json"
+	"strconv"
+	"strings"
 )
 
+/*
+Much appreciated to EndlessCheng
+Adapted from https://github.com/EndlessCheng/codeforces-go/blob/ae5b312f3f/leetcode/testutil/leetcode.go
+*/
+
 type ListNode struct {
-    Val  int
-    Next *ListNode
+	Val  int
+	Next *ListNode
 }
 
 func DeserializeListNode(s string) (*ListNode, error) {
-    var res []*int
-    err := json.Unmarshal([]byte(s), &res)
-    if err != nil {
-        return nil, err
-    }
-    if len(res) == 0 {
-        return nil, nil
-    }
-    root := &ListNode{}
-    n := root
-    for i := 0; i < len(res)-1; i++ {
-        n.Val = *res[i]
-        n.Next = &ListNode{}
-        n = n.Next
-    }
-    n.Val = *res[len(res)-1]
-    return root, nil
+	var res []*int
+	err := json.Unmarshal([]byte(s), &res)
+	if err != nil {
+		return nil, err
+	}
+	if len(res) == 0 {
+		return nil, nil
+	}
+	root := &ListNode{}
+	n := root
+	for i := 0; i < len(res)-1; i++ {
+		n.Val = *res[i]
+		n.Next = &ListNode{}
+		n = n.Next
+	}
+	n.Val = *res[len(res)-1]
+	return root, nil
 }
 
 func (l *ListNode) ToString() string {
-    sb := &strings.Builder{}
-    sb.WriteByte('[')
-    for ; l != nil; l = l.Next {
-        if sb.Len() > 1 {
-            sb.WriteByte(',')
-        }
-        sb.WriteString(strconv.Itoa(l.Val))
-    }
-    sb.WriteByte(']')
-    return sb.String()
+	sb := &strings.Builder{}
+	sb.WriteByte('[')
+	for ; l != nil; l = l.Next {
+		if sb.Len() > 1 {
+			sb.WriteByte(',')
+		}
+		sb.WriteString(strconv.Itoa(l.Val))
+	}
+	sb.WriteByte(']')
+	return sb.String()
 }
 
 func (l *ListNode) Values() []int {
-    vals := []int{}
-    for ; l != nil; l = l.Next {
-        vals = append(vals, l.Val)
-    }
-    return vals
+	vals := []int{}
+	for ; l != nil; l = l.Next {
+		vals = append(vals, l.Val)
+	}
+	return vals
 }
 
 func (l *ListNode) Nodes() []*ListNode {
-    nodes := []*ListNode{}
-    for ; l != nil; l = l.Next {
-        nodes = append(nodes, l)
-    }
-    return nodes
+	nodes := []*ListNode{}
+	for ; l != nil; l = l.Next {
+		nodes = append(nodes, l)
+	}
+	return nodes
 }
 
 type TreeNode struct {
-    Val   int
-    Left  *TreeNode
-    Right *TreeNode
+	Val   int
+	Left  *TreeNode
+	Right *TreeNode
 }
 
 func DeserializeTreeNode(s string) (*TreeNode, error) {
-    var res []*int
-    err := json.Unmarshal([]byte(s), &res)
-    if err != nil {
-        return nil, err
-    }
-    if len(res) == 0 {
-        return nil, nil
-    }
-    nodes := make([]*TreeNode, len(res))
-    for i := 0; i < len(res); i++ {
-        if res[i] != nil {
-            nodes[i] = &TreeNode{Val: *res[i]}
-        }
-    }
-    root := nodes[0]
-    for i, j := 0, 1; j < len(res); i++ {
-        if nodes[i] != nil {
-            nodes[i].Left = nodes[j]
-            j++
-            if j >= len(res) {
-                break
-            }
-            nodes[i].Right = nodes[j]
-            j++
-            if j >= len(res) {
-                break
-            }
-        }
-    }
-    return root, nil
+	var res []*int
+	err := json.Unmarshal([]byte(s), &res)
+	if err != nil {
+		return nil, err
+	}
+	if len(res) == 0 {
+		return nil, nil
+	}
+	nodes := make([]*TreeNode, len(res))
+	for i := 0; i < len(res); i++ {
+		if res[i] != nil {
+			nodes[i] = &TreeNode{Val: *res[i]}
+		}
+	}
+	root := nodes[0]
+	for i, j := 0, 1; j < len(res); i++ {
+		if nodes[i] != nil {
+			nodes[i].Left = nodes[j]
+			j++
+			if j >= len(res) {
+				break
+			}
+			nodes[i].Right = nodes[j]
+			j++
+			if j >= len(res) {
+				break
+			}
+		}
+	}
+	return root, nil
 }
 
 func (t *TreeNode) ToString() string {
-    nodes := []*TreeNode{}
-    queue := []*TreeNode{t}
-    for len(queue) > 0 {
-        t, queue = queue[0], queue[1:]
-        nodes = append(nodes, t)
-        if t != nil {
-            queue = append(queue, t.Left, t.Right)
-        }
-    }
-
-    for len(nodes) > 0 && nodes[len(nodes)-1] == nil {
-        nodes = nodes[:len(nodes)-1]
-    }
-
-    sb := &strings.Builder{}
-    sb.WriteByte('[')
-    for _, node := range nodes {
-        if sb.Len() > 1 {
-            sb.WriteByte(',')
-        }
-        if node != nil {
-            sb.WriteString(strconv.Itoa(node.Val))
-        } else {
-            sb.WriteString("null")
-        }
-    }
-    sb.WriteByte(']')
-    return sb.String()
+	nodes := []*TreeNode{}
+	queue := []*TreeNode{t}
+	for len(queue) > 0 {
+		t, queue = queue[0], queue[1:]
+		nodes = append(nodes, t)
+		if t != nil {
+			queue = append(queue, t.Left, t.Right)
+		}
+	}
+
+	for len(nodes) > 0 && nodes[len(nodes)-1] == nil {
+		nodes = nodes[:len(nodes)-1]
+	}
+
+	sb := &strings.Builder{}
+	sb.WriteByte('[')
+	for _, node := range nodes {
+		if sb.Len() > 1 {
+			sb.WriteByte(',')
+		}
+		if node != nil {
+			sb.WriteString(strconv.Itoa(node.Val))
+		} else {
+			sb.WriteString("null")
+		}
+	}
+	sb.WriteByte(']')
+	return sb.String()
 }
 
-// NaryTreeNode N 叉树
 type NaryTreeNode struct {
-    Val      int
-    Children []*NaryTreeNode
+	Val      int
+	Children []*NaryTreeNode
 }
 
 func DeserializeNaryTreeNode(s string) (*NaryTreeNode, error) {
-    var res []*int
-    if err := json.Unmarshal([]byte(s), &res); err != nil {
-        return nil, err
-    }
-    if len(res) == 0 {
-        return nil, nil
-    }
-    // 用一个伪的头结点
-    root := &NaryTreeNode{}
-    q := []*NaryTreeNode{root}
-    for i := 0; i < len(res); i++ {
-        node := q[0]
-        q = q[1:]
-        for ; i < len(res) && res[i] != nil; i++ {
-            n := &NaryTreeNode{Val: *res[i]}
-            node.Children = append(node.Children, n)
-            q = append(q, n)
-        }
-    }
-
-    return root.Children[0], nil
+	var res []*int
+	if err := json.Unmarshal([]byte(s), &res); err != nil {
+		return nil, err
+	}
+	if len(res) == 0 {
+		return nil, nil
+	}
+	// 用一个伪的头结点
+	root := &NaryTreeNode{}
+	q := []*NaryTreeNode{root}
+	for i := 0; i < len(res); i++ {
+		node := q[0]
+		q = q[1:]
+		for ; i < len(res) && res[i] != nil; i++ {
+			n := &NaryTreeNode{Val: *res[i]}
+			node.Children = append(node.Children, n)
+			q = append(q, n)
+		}
+	}
+
+	return root.Children[0], nil
 }
 
 func (t *NaryTreeNode) ToString() string {
-    nodes := []*NaryTreeNode{}
-    q := []*NaryTreeNode{{Children: []*NaryTreeNode{t}}}
-
-    for len(q) > 0 {
-        node := q[0]
-        q = q[1:]
-        nodes = append(nodes, node)
-
-        if node != nil {
-            if len(node.Children) > 0 {
-                q = append(q, node.Children...)
-            }
-            q = append(q, nil)
-        }
-    }
-    // 去除头结点
-    nodes = nodes[1:]
-    // 去除末尾的 null
-    for len(nodes) > 0 && nodes[len(nodes)-1] == nil {
-        nodes = nodes[:len(nodes)-1]
-    }
-
-    sb := strings.Builder{}
-    sb.WriteByte('[')
-    for _, node := range nodes {
-        if sb.Len() > 1 {
-            sb.WriteByte(',')
-        }
-        if node == nil {
-            sb.WriteString("null")
-        } else {
-            sb.WriteString(strconv.Itoa(node.Val))
-        }
-    }
-    sb.WriteByte(']')
-    return sb.String()
+	nodes := []*NaryTreeNode{}
+	q := []*NaryTreeNode{{Children: []*NaryTreeNode{t}}}
+
+	for len(q) > 0 {
+		node := q[0]
+		q = q[1:]
+		nodes = append(nodes, node)
+
+		if node != nil {
+			if len(node.Children) > 0 {
+				q = append(q, node.Children...)
+			}
+			q = append(q, nil)
+		}
+	}
+	// 去除头结点
+	nodes = nodes[1:]
+	// 去除末尾的 null
+	for len(nodes) > 0 && nodes[len(nodes)-1] == nil {
+		nodes = nodes[:len(nodes)-1]
+	}
+
+	sb := strings.Builder{}
+	sb.WriteByte('[')
+	for _, node := range nodes {
+		if sb.Len() > 1 {
+			sb.WriteByte(',')
+		}
+		if node == nil {
+			sb.WriteString("null")
+		} else {
+			sb.WriteString(strconv.Itoa(node.Val))
+		}
+	}
+	sb.WriteByte(']')
+	return sb.String()
 }
 
 type Node struct {
-    Val   int
-    Prev  *Node
-    Next  *Node
-    Child *Node
+	Val   int
+	Prev  *Node
+	Next  *Node
+	Child *Node
 }
diff --git a/testutils/go/prefdefined_test.go b/testutils/go/prefdefined_test.go
index 06a549cd..2cf01e39 100644
--- a/testutils/go/prefdefined_test.go
+++ b/testutils/go/prefdefined_test.go
@@ -1,102 +1,102 @@
-package common
+package goutils
 
 import (
-    "testing"
+	"testing"
 
-    "github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func Test_NaryTreeNodeToString(t *testing.T) {
-    type testcase struct {
-        tree     *NaryTreeNode
-        expected string
-    }
-    tests := []testcase{
-        {
-            tree:     &NaryTreeNode{Val: 1},
-            expected: "[1]",
-        },
-        {
-            tree: &NaryTreeNode{
-                Val: 1,
-                Children: []*NaryTreeNode{
-                    {
-                        Val: 3,
-                        Children: []*NaryTreeNode{
-                            {Val: 5},
-                            {Val: 6},
-                        },
-                    },
-                    {Val: 2},
-                    {Val: 4},
-                },
-            },
-            expected: "[1,null,3,2,4,null,5,6]",
-        },
-        {
-            tree: &NaryTreeNode{
-                Val: 1,
-                Children: []*NaryTreeNode{
-                    {Val: 2},
-                    {
-                        Val: 3,
-                        Children: []*NaryTreeNode{
-                            {Val: 6},
-                            {
-                                Val: 7,
-                                Children: []*NaryTreeNode{
-                                    {
-                                        Val:      11,
-                                        Children: []*NaryTreeNode{{Val: 14}},
-                                    },
-                                },
-                            },
-                        },
-                    },
-                    {
-                        Val: 4,
-                        Children: []*NaryTreeNode{
-                            {Val: 8, Children: []*NaryTreeNode{{Val: 12}}},
-                        },
-                    },
-                    {
-                        Val: 5,
-                        Children: []*NaryTreeNode{
-                            {
-                                Val:      9,
-                                Children: []*NaryTreeNode{{Val: 13}},
-                            },
-                            {Val: 10},
-                        },
-                    },
-                },
-            },
-            expected: "[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]",
-        },
-    }
-    for _, test := range tests {
-        t.Run(
-            "", func(t *testing.T) {
-                assert.Equal(t, test.expected, test.tree.ToString())
-            },
-        )
-    }
+	type testcase struct {
+		tree     *NaryTreeNode
+		expected string
+	}
+	tests := []testcase{
+		{
+			tree:     &NaryTreeNode{Val: 1},
+			expected: "[1]",
+		},
+		{
+			tree: &NaryTreeNode{
+				Val: 1,
+				Children: []*NaryTreeNode{
+					{
+						Val: 3,
+						Children: []*NaryTreeNode{
+							{Val: 5},
+							{Val: 6},
+						},
+					},
+					{Val: 2},
+					{Val: 4},
+				},
+			},
+			expected: "[1,null,3,2,4,null,5,6]",
+		},
+		{
+			tree: &NaryTreeNode{
+				Val: 1,
+				Children: []*NaryTreeNode{
+					{Val: 2},
+					{
+						Val: 3,
+						Children: []*NaryTreeNode{
+							{Val: 6},
+							{
+								Val: 7,
+								Children: []*NaryTreeNode{
+									{
+										Val:      11,
+										Children: []*NaryTreeNode{{Val: 14}},
+									},
+								},
+							},
+						},
+					},
+					{
+						Val: 4,
+						Children: []*NaryTreeNode{
+							{Val: 8, Children: []*NaryTreeNode{{Val: 12}}},
+						},
+					},
+					{
+						Val: 5,
+						Children: []*NaryTreeNode{
+							{
+								Val:      9,
+								Children: []*NaryTreeNode{{Val: 13}},
+							},
+							{Val: 10},
+						},
+					},
+				},
+			},
+			expected: "[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]",
+		},
+	}
+	for _, test := range tests {
+		t.Run(
+			"", func(t *testing.T) {
+				assert.Equal(t, test.expected, test.tree.ToString())
+			},
+		)
+	}
 }
 
 func Test_DeserializeNaryTree(t *testing.T) {
-    testcases := []string{
-        "[1]",
-        "[1,null,3,2,4,null,5,6]",
-        "[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]",
-    }
-    for _, test := range testcases {
-        t.Run(
-            "", func(t *testing.T) {
-                tree, err := DeserializeNaryTreeNode(test)
-                if assert.NoError(t, err) {
-                    assert.Equal(t, test, tree.ToString())
-                }
-            },
-        )
-    }
+	testcases := []string{
+		"[1]",
+		"[1,null,3,2,4,null,5,6]",
+		"[1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]",
+	}
+	for _, test := range testcases {
+		t.Run(
+			"", func(t *testing.T) {
+				tree, err := DeserializeNaryTreeNode(test)
+				if assert.NoError(t, err) {
+					assert.Equal(t, test, tree.ToString())
+				}
+			},
+		)
+	}
 }
diff --git a/testutils/go/testing.go b/testutils/go/testing.go
deleted file mode 100644
index 08ea938d..00000000
--- a/testutils/go/testing.go
+++ /dev/null
@@ -1,537 +0,0 @@
-package common
-
-/*
-Much appreciated to EndlessCheng
-Copy from https://github.com/EndlessCheng/codeforces-go/blob/ae5b312f3f/leetcode/testutil/leetcode.go
-*/
-
-import (
-	"errors"
-	"fmt"
-	"reflect"
-	"strconv"
-	"strings"
-	"testing"
-)
-
-func parseRawArray(rawArray string) (splits []string, err error) {
-	invalidErr := fmt.Errorf("invalid test data: %s", rawArray)
-
-	// check [] at leftmost and rightmost
-	if len(rawArray) <= 1 || rawArray[0] != '[' || rawArray[len(rawArray)-1] != ']' {
-		return nil, invalidErr
-	}
-
-	// ignore [] at leftmost and rightmost
-	rawArray = rawArray[1 : len(rawArray)-1]
-	if rawArray == "" {
-		return
-	}
-
-	var depth, quote int
-	for i := 0; i < len(rawArray); {
-		j := i
-	outer:
-		for ; j < len(rawArray); j++ {
-			switch rawArray[j] {
-			case '[':
-				depth++
-			case ']':
-				depth--
-			case '"':
-				quote++
-			case ',':
-				if depth == 0 && quote%2 == 0 {
-					break outer
-				}
-			}
-		}
-		splits = append(splits, strings.TrimSpace(rawArray[i:j]))
-		i = j + 1 // skip sep
-	}
-	if depth != 0 || quote%2 != 0 {
-		return nil, invalidErr
-	}
-	return
-}
-
-func parseRawArg(tp reflect.Type, rawData string) (v reflect.Value, err error) {
-	rawData = strings.TrimSpace(rawData)
-	invalidErr := fmt.Errorf("invalid test data: %s", rawData)
-	switch tp.Kind() {
-	case reflect.String:
-		if len(rawData) <= 1 || rawData[0] != '"' && rawData[0] != '\'' || rawData[len(rawData)-1] != rawData[0] {
-			return reflect.Value{}, invalidErr
-		}
-		// 处理转义字符
-		w := strings.Builder{}
-		// remove " (or ') at leftmost and rightmost
-		for i := 1; i < len(rawData)-1; i++ {
-			switch rawData[i] {
-			case '\\':
-				i++
-				switch rawData[i] {
-				case '"':
-					w.WriteByte('"')
-				case '\\':
-					w.WriteByte('\\')
-				case '/':
-					w.WriteByte('/')
-				case 'b':
-					w.WriteByte('\b')
-				case 'f':
-					w.WriteByte('\f')
-				case 'n':
-					w.WriteByte('\n')
-				case 't':
-					w.WriteByte('\t')
-				default:
-					w.WriteByte('\\')
-					w.WriteByte(rawData[i])
-				}
-			default:
-				w.WriteByte(rawData[i])
-			}
-		}
-		v = reflect.ValueOf(w.String())
-	case reflect.Uint8: // byte
-		// rawData like "a" or 'a'
-		if len(rawData) != 3 || rawData[0] != '"' && rawData[0] != '\'' || rawData[2] != rawData[0] {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(rawData[1])
-	case reflect.Int:
-		i, er := strconv.Atoi(rawData)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(i)
-	case reflect.Uint:
-		i, er := strconv.Atoi(rawData)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(uint(i))
-	case reflect.Int64:
-		i, er := strconv.Atoi(rawData)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(int64(i))
-	case reflect.Uint64:
-		i, er := strconv.Atoi(rawData)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(uint64(i))
-	case reflect.Float64:
-		f, er := strconv.ParseFloat(rawData, 64)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(f)
-	case reflect.Bool:
-		b, er := strconv.ParseBool(rawData)
-		if er != nil {
-			return reflect.Value{}, invalidErr
-		}
-		v = reflect.ValueOf(b)
-	case reflect.Slice:
-		splits, er := parseRawArray(rawData)
-		if er != nil {
-			return reflect.Value{}, er
-		}
-		v = reflect.New(tp).Elem()
-		for _, s := range splits {
-			_v, er := parseRawArg(tp.Elem(), s)
-			if er != nil {
-				return reflect.Value{}, er
-			}
-			v = reflect.Append(v, _v)
-		}
-	case reflect.Ptr: // *TreeNode, *ListNode, *Point, *Interval
-		switch tpName := tp.Elem().Name(); tpName {
-		case "TreeNode":
-			root, er := DeserializeTreeNode(rawData)
-			if er != nil {
-				return reflect.Value{}, er
-			}
-			v = reflect.ValueOf(root)
-		case "ListNode":
-			head, er := DeserializeListNode(rawData)
-			if er != nil {
-				return reflect.Value{}, er
-			}
-			v = reflect.ValueOf(head)
-		default:
-			return reflect.Value{}, fmt.Errorf("unknown type %s", tpName)
-		}
-	default:
-		return reflect.Value{}, fmt.Errorf("unknown type %s", tp.Name())
-	}
-	return
-}
-
-func toRawString(v reflect.Value) (s string, err error) {
-	switch v.Kind() {
-	case reflect.Slice:
-		sb := &strings.Builder{}
-		sb.WriteByte('[')
-		for i := 0; i < v.Len(); i++ {
-			if i > 0 {
-				sb.WriteByte(',')
-			}
-			_s, er := toRawString(v.Index(i))
-			if er != nil {
-				return "", er
-			}
-			sb.WriteString(_s)
-		}
-		sb.WriteByte(']')
-		s = sb.String()
-	case reflect.Ptr: // *TreeNode, *ListNode, *Point, *Interval
-		switch tpName := v.Type().Elem().Name(); tpName {
-		case "TreeNode":
-			s = v.Interface().(*TreeNode).ToString()
-		case "ListNode":
-			s = v.Interface().(*ListNode).ToString()
-		default:
-			return "", fmt.Errorf("unknown type %s", tpName)
-		}
-	case reflect.String:
-		s = fmt.Sprintf(`"%s"`, v.Interface())
-	case reflect.Uint8: // byte
-		s = fmt.Sprintf(`"%c"`, v.Interface())
-	case reflect.Float64:
-		s = fmt.Sprintf(`%.5f`, v.Interface())
-	default: // int uint int64 uint64 bool
-		s = fmt.Sprintf(`%v`, v.Interface())
-	}
-	return
-}
-
-// rawExamples[i] = 输入+输出
-// 若反射出来的函数或 rawExamples 数据不合法，则会返回一个非空的 error，否则返回 nil
-
-func RunTests(t *testing.T, f interface{}, examples [][]string, targetCaseNum int) (err error) {
-	t.Helper()
-
-	fType := reflect.TypeOf(f)
-	fValue := reflect.ValueOf(f)
-	if fType.Kind() != reflect.Func {
-		return errors.New("f must be a function")
-	}
-	for caseNo, example := range examples {
-		if len(example) != fType.NumIn()+fType.NumOut() {
-			return fmt.Errorf(
-				"Case#%d invalid: len(example) = %d, but we need %d+%d",
-				caseNo+1,
-				len(example),
-				fType.NumIn(),
-				fType.NumOut(),
-			)
-		}
-	}
-
-	test := func(caseNo int, example []string) (passed bool, err error) {
-		rawIn := example[:fType.NumIn()]
-		ins := make([]reflect.Value, len(rawIn))
-		for i, rawArg := range rawIn {
-			rawArg = trimSpaceAndNewLine(rawArg)
-			ins[i], err = parseRawArg(fType.In(i), rawArg)
-			if err != nil {
-				return
-			}
-		}
-		// just check rawExpectedOuts is valid or not
-		rawExpectedOuts := example[fType.NumIn():]
-		for i := range rawExpectedOuts {
-			rawExpectedOuts[i] = trimSpaceAndNewLine(rawExpectedOuts[i])
-			if _, err = parseRawArg(fType.Out(i), rawExpectedOuts[i]); err != nil {
-				return
-			}
-		}
-
-		const maxInputSize = 150
-		inputInfo := strings.Join(rawIn, "\n")
-		if len(inputInfo) > maxInputSize { // 截断过长的输入
-			inputInfo = inputInfo[:maxInputSize] + "..."
-		}
-
-		subTestName := fmt.Sprintf("Case#%d", caseNo)
-		passed = true
-		t.Run(
-			subTestName, func(t *testing.T) {
-				var outs []reflect.Value
-				if isTLE(func() { outs = fValue.Call(ins) }) {
-					t.Errorf(
-						"Time Limit Exceeded\n"+
-							"input: %s", inputInfo,
-					)
-					return
-				}
-
-				for i, out := range outs {
-					rawActualOut, er := toRawString(out)
-					if er != nil {
-						t.Fatalf("Convert result to string error: %v", er)
-					}
-					if AssertOutput && rawActualOut != rawExpectedOuts[i] {
-						t.Errorf(
-							"Not equal\n"+
-								"input:     %s\n"+
-								"output:    %s\n"+
-								"expected:  %s\n",
-							strings.ReplaceAll(inputInfo, "\n", "↩ "),
-							strings.ReplaceAll(rawActualOut, "\n", "↩ "),
-							strings.ReplaceAll(rawExpectedOuts[i], "\n", "↩ "),
-						)
-						passed = false
-					}
-				}
-			},
-		)
-		return
-	}
-
-	// 例如，-1 表示最后一个测试用例
-	if targetCaseNum < 0 {
-		targetCaseNum += len(examples) + 1
-	}
-	if targetCaseNum > 0 {
-		example := examples[targetCaseNum-1]
-		passed, err := test(targetCaseNum, example)
-		if err != nil {
-			return err
-		}
-		if !passed {
-			return nil
-		}
-		t.Logf("Case#%d passed, continue to run all tests", targetCaseNum)
-	}
-
-	// 如果测试的是单个用例，而且通过了，则继续跑一遍全量用例
-	for curCaseNum, example := range examples {
-		// 跳过已经跑过的目标测试
-		if targetCaseNum > 0 && curCaseNum+1 == targetCaseNum {
-			continue
-		}
-		_, err = test(curCaseNum+1, example)
-		if err != nil {
-			return
-		}
-	}
-	return nil
-}
-
-func RunTestsWithString(t *testing.T, f interface{}, testcases string, targetCaseNum int) (err error) {
-	lines := parseTestCases(testcases)
-	n := len(lines)
-	if n == 0 {
-		return errors.New("invalid testcases: empty testcases")
-	}
-
-	fType := reflect.TypeOf(f)
-	if fType.Kind() != reflect.Func {
-		return errors.New("f must be a function")
-	}
-	// 每 fNumIn+fNumOut 行一组数据
-	fNumIn := fType.NumIn()
-	fNumOut := fType.NumOut()
-	tcSize := fNumIn + fNumOut
-	if n%tcSize != 0 {
-		return fmt.Errorf("invalid testcases: got %d lines, should be a multiple of %d", n, tcSize)
-	}
-
-	examples := make([][]string, 0, n/tcSize)
-	for i := 0; i < n; i += tcSize {
-		examples = append(examples, lines[i:i+tcSize])
-	}
-	return RunTests(t, f, examples, targetCaseNum)
-}
-
-func RunClassTests(t *testing.T, constructor interface{}, examples [][3]string, targetCaseNum int) (err error) {
-	cType := reflect.TypeOf(constructor)
-	cFunc := reflect.ValueOf(constructor)
-	if cType.Kind() != reflect.Func {
-		return fmt.Errorf("constructor must be a function")
-	}
-	if cType.NumOut() != 1 {
-		return fmt.Errorf("constructor must have one and only one return value")
-	}
-
-	test := func(caseNo int, example [3]string) (passed bool, err error) {
-		names := strings.TrimSpace(example[0])
-		inputArgs := strings.TrimSpace(example[1])
-		rawExpectedOut := strings.TrimSpace(example[2])
-		// 去除 rawExpectedOut 中逗号后的空格
-		rawExpectedOut = strings.ReplaceAll(rawExpectedOut, ", ", ",")
-
-		methodNames, err := parseRawArray(names)
-		if err != nil {
-			return
-		}
-		for i, name := range methodNames {
-			name = name[1 : len(name)-1] // 移除引号
-			name = strings.Title(name)   // 首字母大写以匹配模板方法名称
-			methodNames[i] = name
-		}
-
-		rawArgsList, err := parseRawArray(inputArgs)
-		if err != nil {
-			return
-		}
-		if len(rawArgsList) != len(methodNames) {
-			return false, fmt.Errorf(
-				"Case#%d invalid: mismatch names and input args (%d != %d)",
-				caseNo,
-				len(methodNames),
-				len(rawArgsList),
-			)
-		}
-
-		constructorArgs, err := parseRawArray(rawArgsList[0])
-		if err != nil {
-			return
-		}
-		constructorIns := make([]reflect.Value, len(constructorArgs))
-		for i, arg := range constructorArgs {
-			constructorIns[i], err = parseRawArg(cType.In(i), arg)
-			if err != nil {
-				return
-			}
-		}
-
-		obj := cFunc.Call(constructorIns)[0]
-		// use a pointer to call methods
-		pObj := reflect.New(obj.Type())
-		pObj.Elem().Set(obj)
-
-		if DebugCallIndex < 0 {
-			DebugCallIndex += len(rawArgsList)
-		}
-		passed = true
-		subTestName := fmt.Sprintf("Case#%d", caseNo)
-		t.Run(
-			subTestName, func(t *testing.T) {
-				rawActualOut := strings.Builder{}
-				rawActualOut.WriteString("[null")
-
-				for callIndex := 1; callIndex < len(rawArgsList); callIndex++ {
-					name := methodNames[callIndex]
-					method := pObj.MethodByName(name)
-					emptyValue := reflect.Value{}
-					if method == emptyValue {
-						t.Fatalf("invalid test data: method %s not exist", methodNames[callIndex])
-					}
-					methodType := method.Type()
-
-					// parse method input
-					methodArgs, err := parseRawArray(rawArgsList[callIndex])
-					if err != nil {
-						t.Fatalf("invalid test data: invalid input: %v", err)
-					}
-					in := make([]reflect.Value, methodType.NumIn()) // 注意：若入参为空，methodArgs 可能是 [] 也可能是 [null]
-					for i := range in {
-						in[i], err = parseRawArg(methodType.In(i), methodArgs[i])
-						if err != nil {
-							t.Fatalf("invalid test data: invalid input: %v", err)
-						}
-					}
-
-					if callIndex == DebugCallIndex {
-						print() // 在这里打断点
-					}
-
-					// call method
-					var actualOuts []reflect.Value
-					_f := func() { actualOuts = method.Call(in) }
-					if isTLE(_f) {
-						t.Errorf(
-							"Time Limit Exceeded\n"+
-								"Call Index: %d\n"+
-								"Func      : %s(%s)",
-							callIndex,
-							name,
-							rawArgsList[callIndex][1:len(rawArgsList[callIndex])-1],
-						)
-						return
-					}
-
-					if len(actualOuts) > 0 {
-						s, err := toRawString(actualOuts[0])
-						if err != nil {
-							return
-						}
-						rawActualOut.WriteByte(',')
-						rawActualOut.WriteString(s)
-					} else {
-						rawActualOut.WriteString(",null")
-					}
-				}
-				rawActualOut.WriteByte(']')
-				// todo: 提示错在哪个 callIndex 上
-				if AssertOutput && rawExpectedOut != rawActualOut.String() {
-					t.Errorf(
-						"Not equal\n"+
-							"output:     %s\n"+
-							"expected:   %s\n",
-						strings.ReplaceAll(rawExpectedOut, "\n", "↩ "),
-						strings.ReplaceAll(rawActualOut.String(), "\n", "↩ "),
-					)
-					passed = false
-				}
-			},
-		)
-		return
-	}
-
-	// 例如，-1 表示最后一个测试用例
-	if targetCaseNum < 0 {
-		targetCaseNum += len(examples) + 1
-	}
-	if targetCaseNum > 0 {
-		example := examples[targetCaseNum-1]
-		passed, err := test(targetCaseNum, example)
-		if err != nil {
-			return err
-		}
-		if !passed {
-			return nil
-		}
-		t.Logf("Case#%d passed, continue to run all tests", targetCaseNum)
-	}
-
-	// 如果测试的是单个用例，而且通过了，则继续跑一遍全量用例
-	for curCaseNum, example := range examples {
-		// 跳过已经跑过的目标测试
-		if targetCaseNum > 0 && curCaseNum+1 == targetCaseNum {
-			continue
-		}
-		_, err = test(curCaseNum+1, example)
-		if err != nil {
-			return
-		}
-	}
-
-	return nil
-}
-
-func RunClassTestsWithString(t *testing.T, constructor interface{}, testcases string, targetCaseNum int) error {
-	lines := parseTestCases(testcases)
-	n := len(lines)
-	if n == 0 {
-		return fmt.Errorf("invalid testcases: empty testcases")
-	}
-
-	// 每三行一组数据
-	if n%3 != 0 {
-		return fmt.Errorf("invalid testcases: got %d lines, should be a multiple of 3", n)
-	}
-
-	examples := make([][3]string, 0, n/3)
-	for i := 0; i < n; i += 3 {
-		examples = append(examples, [3]string{lines[i], lines[i+1], lines[i+2]})
-	}
-	return RunClassTests(t, constructor, examples, targetCaseNum)
-}
diff --git a/testutils/go/utils.go b/testutils/go/utils.go
index d0407878..11769736 100644
--- a/testutils/go/utils.go
+++ b/testutils/go/utils.go
@@ -1,69 +1,18 @@
-package common
+package goutils
 
 import (
-	"os"
+	"bufio"
 	"strings"
-	"time"
-
-	"github.com/mitchellh/go-ps"
 )
 
-// IsDebugging will return true if the process was launched from Delve or the
-// gopls language server debugger.
-//
-// It does not detect situations where a debugger attached after process start.
-func IsDebugging() bool {
-	pid := os.Getppid()
-
-	// We loop in case there were intermediary processes like the gopls language server.
-	for pid != 0 {
-		switch p, err := ps.FindProcess(pid); {
-		case p == nil || err != nil:
-			return false
-		case strings.HasPrefix(p.Executable(), "dlv"):
-			return true
-		default:
-			pid = p.PPid()
-		}
+func ReadLine(r *bufio.Reader) string {
+	if line, err := r.ReadString('\n'); err != nil {
+		panic(err)
+	} else {
+		return line
 	}
-	return false
 }
 
-func isTLE(f func()) bool {
-	if DebugTLE == 0 || IsDebugging() {
-		f()
-		return false
-	}
-
-	done := make(chan struct{})
-	go func() {
-		defer close(done)
-		f()
-	}()
-	select {
-	case <-done:
-		return false
-	case <-time.After(DebugTLE):
-		return true
-	}
-}
-
-func trimSpaceAndNewLine(s string) string {
-	lines := strings.Split(strings.TrimSpace(s), "\n")
-	for i, line := range lines {
-		lines[i] = strings.TrimSpace(line)
-	}
-	return strings.Join(lines, "")
-}
-
-func parseTestCases(s string) (res []string) {
-	lines := strings.Split(s, "\n")
-	for _, line := range lines {
-		line = strings.TrimSpace(line)
-		if line == "" || strings.HasPrefix(line, "input:") || strings.HasPrefix(line, "output:") {
-			continue
-		}
-		res = append(res, line)
-	}
-	return
+func JoinArray(s []string) string {
+	return "[" + strings.Join(s, ",") + "]"
 }