Skip to content

Commit 0ae772a

Browse files
committed
test: add unit tests for pure functions
test: add unit tests for pure functions
1 parent bdd2081 commit 0ae772a

11 files changed

Lines changed: 463 additions & 0 deletions

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
# ソースコード
55
!.gitignore
66
!README.md
7+
!.github/
8+
!.github/**
79
!LICENSE
810
!go.mod
11+
!package.json
912
!main.go
1013
!server.go
1114
!api/
@@ -20,3 +23,5 @@
2023
!static/css/**
2124
!static/js/
2225
!static/js/**
26+
!test/
27+
!test/**

README.md

Lines changed: 2 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 など)を読む際に、検索結果をグラフに積み上げながら構造を把握していくことを想定しています。

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/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/js/editor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,3 +800,5 @@ addEventListener('DOMContentLoaded', () => {
800800
id('pane-search').style.height = savedLeftH + 'px';
801801
}
802802
});
803+
804+
if (typeof module !== 'undefined') module.exports = { fzfMatchToken, fzfScore, fzfFilter, buildDefinitionParams };

static/js/graph.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,3 +1399,5 @@ function attachIndentDrag(handle, row, nodeId, depth) {
13991399
}
14001400

14011401
// インクルード依存グラフ機能は static/js/include-graph.js に分離されています。
1402+
1403+
if (typeof module !== 'undefined') module.exports = { computeDepths, findParent, findGrandparent, clientIsDescendant, getNodeSiblings };

static/js/search.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,5 @@ function initSearchBar() {
678678
id('btn-toggle-sub').classList.toggle('open', open);
679679
};
680680
}
681+
682+
if (typeof module !== 'undefined') module.exports = { buildSearchParams, nextResultIndex, buildSearchSummary, upsertSearchTab };

test/editor.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const { test } = require('node:test');
2+
const assert = require('node:assert/strict');
3+
4+
// stub browser globals required by editor.js at module load time
5+
global.addEventListener = () => {};
6+
global.id = () => null;
7+
8+
const { fzfMatchToken, fzfScore, fzfFilter, buildDefinitionParams } = require('../static/js/editor.js');
9+
10+
test('fzfMatchToken - exact match', () => {
11+
const r = fzfMatchToken('foobar', 'foo');
12+
assert.ok(r !== null);
13+
assert.ok(r.score > 0);
14+
});
15+
16+
test('fzfMatchToken - no match', () => {
17+
assert.equal(fzfMatchToken('foobar', 'xyz'), null);
18+
});
19+
20+
test('fzfMatchToken - consecutive chars score higher', () => {
21+
const consecutive = fzfMatchToken('foobar', 'foo');
22+
const scattered = fzfMatchToken('fxoxo', 'foo');
23+
assert.ok(consecutive.score > scattered.score);
24+
});
25+
26+
test('fzfScore - single token match', () => {
27+
assert.ok(fzfScore('src/main.c', 'main') > 0);
28+
});
29+
30+
test('fzfScore - multi token AND', () => {
31+
assert.ok(fzfScore('src/main.c', 'src main') > 0);
32+
});
33+
34+
test('fzfScore - token not found returns -1', () => {
35+
assert.equal(fzfScore('src/main.c', 'xyz'), -1);
36+
});
37+
38+
test('fzfScore - empty query returns 0', () => {
39+
assert.equal(fzfScore('src/main.c', ''), 0);
40+
});
41+
42+
test('fzfFilter - returns top N results', () => {
43+
const files = ['a.c', 'b.c', 'c.c', 'd.c', 'e.c'];
44+
const result = fzfFilter(files, '', 3);
45+
assert.equal(result.length, 3);
46+
});
47+
48+
test('fzfFilter - filters and sorts by score', () => {
49+
const files = ['openssl/bio.c', 'openssl/ssl.c', 'curl/easy.c'];
50+
const result = fzfFilter(files, 'ssl', 10);
51+
assert.ok(result.every(f => f.includes('ssl')));
52+
});
53+
54+
test('fzfFilter - no match returns empty', () => {
55+
const result = fzfFilter(['a.c', 'b.c'], 'xyz', 10);
56+
assert.equal(result.length, 0);
57+
});
58+
59+
test('buildDefinitionParams - basic', () => {
60+
const p = buildDefinitionParams('foo', '', '', false);
61+
assert.ok(p.get('q').includes('foo'));
62+
assert.equal(p.get('regex'), '1');
63+
assert.equal(p.get('case'), '0');
64+
});
65+
66+
test('buildDefinitionParams - case sensitive', () => {
67+
const p = buildDefinitionParams('Foo', '', '', true);
68+
assert.equal(p.get('case'), '1');
69+
});
70+
71+
test('buildDefinitionParams - with dir and glob', () => {
72+
const p = buildDefinitionParams('bar', 'src', '*.h', false);
73+
assert.equal(p.get('dir'), 'src');
74+
assert.equal(p.get('glob'), '*.h');
75+
});
76+
77+
test('buildDefinitionParams - escapes regex special chars', () => {
78+
const p = buildDefinitionParams('foo.bar', '', '', false);
79+
assert.ok(p.get('q').includes('foo\\.bar'));
80+
});

0 commit comments

Comments
 (0)