main
1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import assert from 'node:assert'
16import { test, describe, beforeEach, before, after } from 'node:test'
17import { formatCmd, log } from '../src/log.ts'
18
19describe('log', () => {
20 describe('log()', () => {
21 const data = []
22 const stream = {
23 write(s: string) {
24 data.push(s)
25 },
26 } as NodeJS.WriteStream
27
28 before(() => (log.output = stream))
29
30 after(() => {
31 delete log.output
32 delete log.formatters
33 })
34
35 beforeEach(() => (data.length = 0))
36
37 test('empty log', () => {
38 log({
39 kind: 'cmd',
40 cmd: 'echo hi',
41 cwd: process.cwd(),
42 id: '1',
43 verbose: false,
44 })
45 assert.equal(data.join(''), '')
46 })
47
48 test('cmd', () => {
49 log({
50 kind: 'cmd',
51 cmd: 'echo hi',
52 cwd: process.cwd(),
53 id: '1',
54 verbose: true,
55 })
56 assert.equal(data.join(''), '$ \x1B[92mecho\x1B[39m hi\n')
57 })
58
59 test('stdout', () => {
60 log({
61 kind: 'stdout',
62 data: Buffer.from('foo'),
63 id: '1',
64 verbose: true,
65 })
66 assert.equal(data.join(''), 'foo')
67 })
68
69 test('cd', () => {
70 log({
71 kind: 'cd',
72 dir: '/tmp',
73 verbose: true,
74 })
75 assert.equal(data.join(''), '$ \x1B[92mcd\x1B[39m /tmp\n')
76 })
77
78 test('fetch', () => {
79 log({
80 kind: 'fetch',
81 url: 'https://example.com',
82 init: { method: 'GET' },
83 verbose: true,
84 })
85 assert.equal(
86 data.join(''),
87 "$ \x1B[92mfetch\x1B[39m https://example.com { method: 'GET' }\n"
88 )
89 })
90
91 test('custom', () => {
92 log({
93 kind: 'custom',
94 data: 'test',
95 verbose: true,
96 })
97 assert.equal(data.join(''), 'test')
98 })
99
100 test('retry', () => {
101 log({
102 kind: 'retry',
103 attempt: 1,
104 total: 3,
105 delay: 1000,
106 exception: new Error('foo'),
107 error: 'bar',
108 verbose: true,
109 })
110 assert.equal(
111 data.join(''),
112 '\x1B[41m\x1B[37m FAIL \x1B[39m\x1B[49m Attempt: 1/3; next in 1000ms\n'
113 )
114 })
115
116 test('end', () => {
117 log({
118 kind: 'end',
119 id: '1',
120 exitCode: null,
121 signal: null,
122 duration: 0,
123 error: null,
124 verbose: true,
125 })
126 assert.equal(data.join(''), '')
127 })
128
129 test('kill', () => {
130 log({
131 kind: 'kill',
132 signal: null,
133 pid: 1234,
134 })
135 assert.equal(data.join(''), '')
136 })
137
138 test('formatters', () => {
139 log.formatters = {
140 cmd: ({ cmd }) => `CMD: ${cmd}`,
141 }
142
143 log({
144 kind: 'cmd',
145 cmd: 'echo hi',
146 cwd: process.cwd(),
147 id: '1',
148 verbose: true,
149 })
150 assert.equal(data.join(''), 'CMD: echo hi')
151 })
152 })
153
154 test('formatCwd()', () => {
155 const cases = [
156 [
157 `echo $'hi'`,
158 "$ \x1B[92mecho\x1B[39m \x1B[93m$\x1B[39m\x1B[93m'hi'\x1B[39m\n",
159 ],
160 [`echo$foo`, '$ \x1B[92mecho\x1B[39m\x1B[93m$\x1B[39mfoo\n'],
161 [
162 `test --foo=bar p1 p2`,
163 '$ \x1B[92mtest\x1B[39m --foo\x1B[31m=\x1B[39mbar p1 p2\n',
164 ],
165 [
166 `cmd1 --foo || cmd2`,
167 '$ \x1B[92mcmd1\x1B[39m --foo \x1B[31m|\x1B[39m\x1B[31m|\x1B[39m\x1B[92m cmd2\x1B[39m\n',
168 ],
169 [
170 `A=B C='D' cmd`,
171 "$ A\x1B[31m=\x1B[39mB C\x1B[31m=\x1B[39m\x1B[93m'D'\x1B[39m\x1B[92m cmd\x1B[39m\n",
172 ],
173 [
174 `foo-extra --baz = b-a-z --bar = 'b-a-r' -q -u x`,
175 "$ \x1B[92mfoo-extra\x1B[39m --baz \x1B[31m=\x1B[39m b-a-z --bar \x1B[31m=\x1B[39m \x1B[93m'b-a-r'\x1B[39m -q -u x\n",
176 ],
177 [
178 `while true; do "$" done`,
179 '$ \x1B[96mwhile\x1B[39m true\x1B[31m;\x1B[39m\x1B[96m do\x1B[39m \x1B[93m"$"\x1B[39m\x1B[96m done\x1B[39m\n',
180 ],
181 [
182 `echo '\n str\n'`,
183 "$ \x1B[92mecho\x1B[39m \x1B[93m'\x1B[39m\x1B[0m\x1B[0m\n\x1B[0m> \x1B[0m\x1B[93m str\x1B[39m\x1B[0m\x1B[0m\n\x1B[0m> \x1B[0m\x1B[93m'\x1B[39m\n",
184 ],
185 [`$'\\''`, "$ \x1B[93m$\x1B[39m\x1B[93m'\\'\x1B[39m\x1B[93m'\x1B[39m\n"],
186 [
187 'sass-compiler --style=compressed src/static/bootstrap.scss > dist/static/bootstrap-v5.3.3.min.css',
188 '$ \x1B[92msass-compiler\x1B[39m --style\x1B[31m=\x1B[39mcompressed src/static/bootstrap.scss \x1B[31m>\x1B[39m\x1B[92m dist/static/bootstrap-v5.3.3.min.css\x1B[39m\n',
189 ],
190 [
191 'echo 1+2 | bc',
192 '$ \x1B[92mecho\x1B[39m 1\x1B[31m+\x1B[39m2 \x1B[31m|\x1B[39m\x1B[92m bc\x1B[39m\n',
193 ],
194 [
195 'echo test &>> filepath',
196 '$ \x1B[92mecho\x1B[39m test \x1B[31m&\x1B[39m\x1B[31m>\x1B[39m\x1B[31m>\x1B[39m\x1B[92m filepath\x1B[39m\n',
197 ],
198 [
199 'bc < filepath',
200 '$ \x1B[92mbc\x1B[39m \x1B[31m<\x1B[39m\x1B[92m filepath\x1B[39m\n',
201 ],
202 [
203 `cat << 'EOF' | tee -a filepath
204line 1
205line 2
206EOF`,
207 "$ \x1B[92mcat\x1B[39m \x1B[31m<\x1B[39m\x1B[31m<\x1B[39m \x1B[93m'EOF'\x1B[39m \x1B[31m|\x1B[39m\x1B[92m tee\x1B[39m -a filepath\x1B[0m\x1B[0m\n\x1B[0m> \x1B[0mline 1\x1B[0m\x1B[0m\n\x1B[0m> \x1B[0mline 2\x1B[96m\x1B[39m\x1B[0m\x1B[0m\n\x1B[0m> \x1B[0m\x1B[96mEOF\x1B[39m\n",
208 ],
209 ]
210
211 cases.forEach(([input, expected]) => {
212 assert.equal(formatCmd(input), expected, input)
213 })
214 })
215})