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})