Skip to content

Commit 5335d34

Browse files
committed
test: add unit tests for pure functions
test: add unit tests for pure functions
1 parent bdd2081 commit 5335d34

19 files changed

Lines changed: 1478 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
js:
11+
name: JS unit tests
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 22
18+
- run: node --test test/*.test.js
19+
20+
go:
21+
name: Go tests
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: actions/setup-go@v5
26+
with:
27+
go-version-file: go.mod
28+
- run: go test ./...

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
# ソースコード
55
!.gitignore
66
!README.md
7+
!.github/
8+
!.github/**
79
!LICENSE
810
!go.mod
11+
!go.sum
12+
!package.json
913
!main.go
1014
!server.go
1115
!api/
@@ -20,3 +24,7 @@
2024
!static/css/**
2125
!static/js/
2226
!static/js/**
27+
!static/addons/
28+
!static/addons/**
29+
!test/
30+
!test/**

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# grepnavi
22

3+
[![CI](https://github.com/Taka-S-dev/grepnavi/actions/workflows/test.yml/badge.svg)](https://github.com/Taka-S-dev/grepnavi/actions/workflows/test.yml)
4+
35
コードベース調査ツール。ripgrep の高速検索 + Monaco エディタ + 調査グラフで、**「どこを調べたか」を記録しながらコードを読み解く**ためのツールです。
46

57
大きなコードベース(Linux カーネル、OpenSSL、curl など)を読む際に、検索結果をグラフに積み上げながら構造を把握していくことを想定しています。
@@ -45,6 +47,30 @@
4547
- **ドラッグ&ドロップ** — ノードを別ノードにドロップして親子関係を変更。drop-before / drop-after のライン表示で挿入位置を確認
4648
- **⠿ ハンドル** — ノード行の左端ハンドルを左右にドラッグしてインデントレベル(階層)を変更
4749
- **キーボードでの移動** — Shift+Alt+Arrow キーで選択ノードを移動(↑↓ で兄弟間、← でレベルアップ、→ でレベルダウン)
50+
- **↑ / ↓ で選択変更** — ツリービュー上でフォーカス外(テキスト入力・エディタ以外)のとき、Up/Down 矢印キーで選択ノードを切り替え
51+
52+
### アドオン
53+
54+
アドオンは `static/addons/<name>/addon.js` + `addon.css` で実装されており、`static/addons/addons.js``ADDONS` 配列に名前を列挙するだけで有効化できます。
55+
56+
#### コールツリー
57+
58+
関数の呼び出し元(callers)/ 呼び出し先(callees)をツリー形式で表示します。
59+
60+
- エディタ上の関数名を選択して右クリック or ツールバーの「Call Tree」ボタンで起動
61+
- 入力欄に関数名を入力して実行(Enter または Go)
62+
- `callers` / `callees` タブで方向を切り替え
63+
- ノードをクリックしてソースへジャンプ。`` で再帰的に展開
64+
- `?mode=panel` 時はサイドパネルのタブとして自己登録
65+
66+
#### C インクルード依存グラフ
67+
68+
`#include` の依存関係を D3.js フォースグラフで可視化します。
69+
70+
- エディタで C/C++ ファイルを開くと起点ファイルが自動セット
71+
- ツールバーの「Include Graph」ボタンでパネルを開き「Analyze」で解析開始
72+
- ノードをクリックして上流・下流を展開。Ctrl+クリック or ダブルクリックでエディタに表示
73+
- 「Collapse All」でグラフを折りたたみ
4874

4975
### プロジェクト
5076

@@ -64,11 +90,29 @@
6490

6591
## インストール・起動
6692

93+
### バイナリをダウンロード(推奨)
94+
95+
[GitHub Releases](https://github.com/Taka-S-dev/grepnavi/releases) からアーカイブをダウンロードして展開してください。Go のインストール不要です。
96+
97+
```
98+
grepnavi/
99+
├── grepnavi.exe ← 実行ファイル
100+
└── static/ ← 静的ファイル(exe と同じディレクトリに必須)
101+
```
102+
103+
> **注意:** `static/` フォルダを相対パスで参照するため、exe 単体では動作しません。アーカイブを展開したディレクトリごと配置してください。
104+
105+
### ソースからビルド
106+
67107
```bash
68108
# ビルド
69109
go build .
70110
# → Windows: grepnavi.exe Mac/Linux: grepnavi が生成されます
111+
```
71112

113+
### 起動
114+
115+
```bash
72116
# 起動(カレントディレクトリを検索ルートとして使用)
73117
.\grepnavi.exe # Windows
74118
./grepnavi # Mac/Linux
@@ -143,6 +187,7 @@ var require = {paths: {'vs': '/vs'}};
143187
| `Ctrl+Z` | 元に戻す |
144188
| `Ctrl+Shift+N` | 新しいウィンドウを開く |
145189
| `F12` / `Ctrl+クリック` | 定義ジャンプ |
190+
| `` / `` | ツリービューで選択ノードを変更 |
146191
| `Shift+Alt+↑` / `` | 選択ノードを上 / 下に移動 |
147192
| `Shift+Alt+←` / `` | 選択ノードのレベルを上げる / 下げる |
148193
| `?` | キーボードショートカット一覧を表示 |
@@ -244,6 +289,9 @@ grepnavi/
244289
│ ├── ripgrep.go # ripgrep 呼び出し・JSON パース・SSE ストリーミング
245290
│ ├── definition.go # 定義ジャンプ(ctags 風シンボル解析)
246291
│ ├── symbols.go # シンボル抽出
292+
│ ├── hover.go # ホバープレビュー用スニペット取得
293+
│ ├── calltree.go # callers / callees 解析
294+
│ ├── include.go # C インクルード依存グラフ解析
247295
│ ├── ifdef.go # #ifdef 条件コンパイル解析
248296
│ └── ifdef_eval.go # #ifdef 条件評価
249297
└── static/
@@ -255,8 +303,13 @@ grepnavi/
255303
│ ├── search.js # 検索・フィルタ・結果表示
256304
│ ├── graph.js # グラフ/ツリー操作・D3.js・詳細パネル・D&D
257305
│ ├── editor.js # Monaco エディタ・fzf・ナビ履歴・行メモ・#ifdef
306+
│ ├── include-graph.js # C インクルード依存グラフ(D3.js)
258307
│ ├── project.js # プロジェクト保存/開く・ルート設定・glob履歴・リサイザー
259308
│ └── app.js # ブートストラップ・グローバルイベント登録
309+
├── addons/
310+
│ ├── addons.js # アドオン設定(有効化リスト)
311+
│ ├── c-include/ # C インクルード依存グラフ アドオン
312+
│ └── call-tree/ # コールツリー アドオン
260313
└── css/
261314
└── main.css
262315
```
@@ -284,6 +337,12 @@ grepnavi/
284337
| `GET` | `/api/open` | ファイルの内容取得 |
285338
| `GET` | `/api/snippet` | スニペット取得 |
286339
| `GET` | `/api/definition` | 定義ジャンプ先の検索 |
340+
| `GET` | `/api/hover` | ホバープレビュー用スニペット取得 |
341+
| `GET` | `/api/callers` | 関数の呼び出し元を検索 |
342+
| `GET` | `/api/callees` | 関数の呼び出し先を検索 |
343+
| `GET` | `/api/include-graph` | ファイルのインクルード依存グラフ取得 |
344+
| `GET` | `/api/include-file` | ファイルが `#include` しているファイル一覧 |
345+
| `GET` | `/api/include-by` | ファイルを `#include` しているファイル一覧 |
287346

288347
---
289348

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module grepnavi
22

33
go 1.25.0
44

5-
require golang.org/x/text v0.35.0 // indirect
5+
require golang.org/x/text v0.35.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
2+
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "ripgrep",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "node --test test/*.test.js"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"type": "commonjs",
13+
"dependencies": {
14+
"monaco-editor": "^0.55.1"
15+
}
16+
}

search/hover.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ func FindHover(ctx context.Context, word, dir, glob string) ([]HoverHit, error)
7171
if body == "" {
7272
body = h.Text
7373
}
74+
if comment := extractLeadingComment(lines, h.Line); comment != "" {
75+
body = comment + "\n" + body
76+
}
7477
result = append(result, HoverHit{File: h.File, Line: h.Line, Kind: h.Kind, Body: body})
7578
}
7679

@@ -225,6 +228,26 @@ func extractBraceBlockBackward(lines []string, endLine int) string {
225228
return stripCommonIndent(lines[startIdx : idx+1])
226229
}
227230

231+
// extractLeadingComment は startLine(1-indexed)の直前にあるコメントブロックを返す。
232+
// C スタイル(// / /* */)に対応。空行またはコメント以外の行が来たら打ち切る。
233+
func extractLeadingComment(lines []string, startLine int) string {
234+
var commentLines []string
235+
for i := startLine - 2; i >= 0; i-- {
236+
trimmed := strings.TrimSpace(lines[i])
237+
if trimmed == "" {
238+
break
239+
}
240+
if strings.HasPrefix(trimmed, "//") ||
241+
strings.HasPrefix(trimmed, "*") ||
242+
strings.HasPrefix(trimmed, "/*") {
243+
commentLines = append([]string{lines[i]}, commentLines...)
244+
} else {
245+
break
246+
}
247+
}
248+
return strings.Join(commentLines, "\n")
249+
}
250+
228251
// extractDefineBlock は #define の継続行(末尾 \)を含めて抽出する。
229252
func extractDefineBlock(lines []string, startLine int) string {
230253
idx := startLine - 1

search/ifdef_eval_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package search
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseDefines(t *testing.T) {
8+
tests := []struct {
9+
input string
10+
want map[string]int
11+
}{
12+
{"WIN32=1 DEBUG=0", map[string]int{"WIN32": 1, "DEBUG": 0}},
13+
{"NDEBUG", map[string]int{"NDEBUG": 1}},
14+
{"A=1 B=2 C", map[string]int{"A": 1, "B": 2, "C": 1}},
15+
{"", map[string]int{}},
16+
}
17+
for _, tt := range tests {
18+
got := ParseDefines(tt.input)
19+
if len(got) != len(tt.want) {
20+
t.Errorf("ParseDefines(%q): got %v, want %v", tt.input, got, tt.want)
21+
continue
22+
}
23+
for k, v := range tt.want {
24+
if got[k] != v {
25+
t.Errorf("ParseDefines(%q)[%q]: got %d, want %d", tt.input, k, got[k], v)
26+
}
27+
}
28+
}
29+
}
30+
31+
func TestEvalIfExpr(t *testing.T) {
32+
defines := map[string]int{"WIN32": 1, "DEBUG": 0, "VERSION": 2}
33+
34+
tests := []struct {
35+
expr string
36+
want bool
37+
}{
38+
{"WIN32", true},
39+
{"DEBUG", false},
40+
{"!WIN32", false},
41+
{"!DEBUG", true},
42+
{"WIN32 == 1", true},
43+
{"WIN32 == 0", false},
44+
{"VERSION == 2", true},
45+
{"WIN32 && !DEBUG", true},
46+
{"WIN32 || DEBUG", true},
47+
{"DEBUG || 0", false},
48+
{"defined(WIN32)", true},
49+
{"defined(UNKNOWN)", false},
50+
{"(WIN32 && DEBUG) || VERSION", true},
51+
}
52+
for _, tt := range tests {
53+
got := evalIfExpr(tt.expr, defines)
54+
if got != tt.want {
55+
t.Errorf("evalIfExpr(%q): got %v, want %v", tt.expr, got, tt.want)
56+
}
57+
}
58+
}
59+
60+
func TestComputeInactiveSet(t *testing.T) {
61+
t.Run("ifdef active branch", func(t *testing.T) {
62+
lines := []string{
63+
"#ifdef WIN32",
64+
"int x = 1;",
65+
"#else",
66+
"int x = 2;",
67+
"#endif",
68+
}
69+
defines := map[string]int{"WIN32": 1}
70+
inactive := computeInactiveSet(lines, defines)
71+
if inactive[2] {
72+
t.Error("line 2 should be active (WIN32 branch)")
73+
}
74+
if !inactive[4] {
75+
t.Error("line 4 should be inactive (else branch)")
76+
}
77+
})
78+
79+
t.Run("ifdef inactive branch", func(t *testing.T) {
80+
lines := []string{
81+
"#ifdef LINUX",
82+
"int x = 1;",
83+
"#else",
84+
"int x = 2;",
85+
"#endif",
86+
}
87+
defines := map[string]int{"WIN32": 1}
88+
inactive := computeInactiveSet(lines, defines)
89+
if !inactive[2] {
90+
t.Error("line 2 should be inactive (LINUX not defined)")
91+
}
92+
if inactive[4] {
93+
t.Error("line 4 should be active (else branch)")
94+
}
95+
})
96+
97+
t.Run("ifndef", func(t *testing.T) {
98+
lines := []string{
99+
"#ifndef NDEBUG",
100+
"assert(x);",
101+
"#endif",
102+
}
103+
defines := map[string]int{}
104+
inactive := computeInactiveSet(lines, defines)
105+
if inactive[2] {
106+
t.Error("line 2 should be active (NDEBUG not defined)")
107+
}
108+
})
109+
110+
t.Run("nested ifdef", func(t *testing.T) {
111+
lines := []string{
112+
"#ifdef WIN32",
113+
"#ifdef DEBUG",
114+
"log();",
115+
"#endif",
116+
"#endif",
117+
}
118+
defines := map[string]int{"WIN32": 1}
119+
inactive := computeInactiveSet(lines, defines)
120+
if !inactive[3] {
121+
t.Error("line 3 should be inactive (DEBUG not defined)")
122+
}
123+
})
124+
125+
t.Run("elif", func(t *testing.T) {
126+
lines := []string{
127+
"#if VERSION == 1",
128+
"v1();",
129+
"#elif VERSION == 2",
130+
"v2();",
131+
"#else",
132+
"vx();",
133+
"#endif",
134+
}
135+
defines := map[string]int{"VERSION": 2}
136+
inactive := computeInactiveSet(lines, defines)
137+
if !inactive[2] {
138+
t.Error("line 2 should be inactive (VERSION != 1)")
139+
}
140+
if inactive[4] {
141+
t.Error("line 4 should be active (VERSION == 2)")
142+
}
143+
if !inactive[6] {
144+
t.Error("line 6 should be inactive (else)")
145+
}
146+
})
147+
}

static/addons/addons.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// ===== アドオン設定 =====
2+
// 使いたいアドオン名を列挙する。コメントアウトで無効化。
3+
// 新しいアドオンは addons/<name>/ にファイルを置いてここに追記する。
4+
const ADDONS = [
5+
'c-include', // C言語インクルード依存グラフ
6+
'call-tree', // コールツリーパネル
7+
];

0 commit comments

Comments
 (0)