Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion packages/internals/src/__tests__/schemaEngineCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import tempy from 'tempy'
import { describe, expect, test, vi } from 'vitest'

import { credentialsToUri, uriToCredentials } from '../convertCredentials'
import { canConnectToDatabase, createDatabase, dropDatabase, execaCommand } from '../schemaEngineCommands'
import {
canConnectToDatabase,
createDatabase,
dropDatabase,
execaCommand,
formatSchemaEngineError,
parseJsonFromStderr,
type SchemaEngineLogLine,
} from '../schemaEngineCommands'

if (process.env.CI) {
// 5s is often not enough for the "postgresql - create database" test on macOS CI.
Expand All @@ -29,6 +37,47 @@ describe('execaCommand', () => {
})
})

describe('formatSchemaEngineError', () => {
const log = (message: string): SchemaEngineLogLine => ({
timestamp: '2021-06-11T15:35:34.084486+00:00',
level: 'ERROR',
target: 'schema_engine::logger',
fields: { message },
})

test('joins messages from multiple log lines', () => {
expect(formatSchemaEngineError([log('first'), log('second')], 'raw stderr')).toBe('first\nsecond')
})

test('falls back to the raw stderr when no log line has a message', () => {
// e.g. when parseJsonFromStderr's `.slice(1)` drops the engine's only stderr line,
// leaving no logs to extract a message from.
expect(formatSchemaEngineError([], 'the only line of stderr, with the real error')).toBe(
'the only line of stderr, with the real error',
)
})

test('falls back to the raw stderr when log lines have empty messages', () => {
expect(formatSchemaEngineError([log('')], 'raw stderr')).toBe('raw stderr')
})

test('falls back to the raw stderr when log lines have whitespace-only messages', () => {
expect(formatSchemaEngineError([log(' ')], 'raw stderr')).toBe('raw stderr')
})
})

describe('parseJsonFromStderr', () => {
test('does not throw on a single-line stderr with a trailing newline', () => {
// stderr.split(/\r?\n/).slice(1) on "real error\n" leaves [''], which used to
// reach JSON.parse('') and throw before formatSchemaEngineError's fallback ever ran.
expect(parseJsonFromStderr('real error\n')).toEqual([])
})

test('does not throw on a single-line stderr with no trailing newline', () => {
expect(parseJsonFromStderr('real error')).toEqual([])
})
})
Comment on lines +40 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Place the new test suite alongside the source file.

This *.test.ts suite is under packages/internals/src/__tests__, not alongside packages/internals/src/schemaEngineCommands.ts. Move the added tests to an adjacent test file, using the required kebab-case name if a new file is created.

As per coding guidelines, *.test.ts files should be placed alongside source files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/internals/src/__tests__/schemaEngineCommands.test.ts` around lines
39 - 62, The formatSchemaEngineError tests are located in the separate
src/__tests__ directory instead of alongside their source module. Move this test
suite next to schemaEngineCommands.ts, using the repository’s required
kebab-case test filename, while preserving all existing test coverage.

Source: Coding guidelines


describe('canConnectToDatabase', () => {
test('sqlite - can', async () => {
await expect(canConnectToDatabase('file:./introspection/blog.db', __dirname)).resolves.toEqual(true)
Expand Down
25 changes: 20 additions & 5 deletions packages/internals/src/schemaEngineCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ export interface ConnectionError {
code: DatabaseErrorCodes
}

function parseJsonFromStderr(stderr: string): SchemaEngineLogLine[] {
export function parseJsonFromStderr(stderr: string): SchemaEngineLogLine[] {
// split by new line
const lines = stderr.split(/\r?\n/).slice(1) // Remove first element
const lines = stderr
.split(/\r?\n/)
.slice(1) // Remove first element
.filter((line) => line.trim() !== '') // A trailing newline leaves a blank line that isn't valid JSON
const logs: any = []

for (const line of lines) {
Expand All @@ -64,6 +67,18 @@ function parseJsonFromStderr(stderr: string): SchemaEngineLogLine[] {
return logs
}

/**
* `parseJsonFromStderr` drops the engine's first stderr line as a discardable
* preamble. When the engine only emits that one line for a given failure, the
* only line with real information is dropped, `logs` ends up empty, and this
* used to produce a bare "Schema engine error:" with nothing after it. Fall
* back to the raw stderr so the error always carries some diagnostic content.
*/
export function formatSchemaEngineError(logs: SchemaEngineLogLine[], stderr: string): string {
const messages = logs.map((log) => log.fields.message).filter((message) => Boolean(message?.trim()))
return messages.length > 0 ? messages.join('\n') : stderr
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// could be refactored with engines using JSON RPC instead and just passing the schema
export async function canConnectToDatabase(
connectionString: string,
Expand Down Expand Up @@ -94,7 +109,7 @@ export async function canConnectToDatabase(
message: error.fields.message,
}
} else {
throw new Error(`Schema engine error:\n${logs.map((log) => log.fields.message).join('\n')}`)
throw new Error(`Schema engine error:\n${formatSchemaEngineError(logs, e.stderr)}`)
}
} else {
throw new Error(`Schema engine exited. ${_e}`)
Expand Down Expand Up @@ -132,7 +147,7 @@ export async function createDatabase(connectionString: string, cwd = process.cwd
if (error && error.fields.error_code && error.fields.message) {
throw new Error(`${error.fields.error_code}: ${error.fields.message}`)
} else {
throw new Error(`Schema engine error:\n${logs.map((log) => log.fields.message).join('\n')}`)
throw new Error(`Schema engine error:\n${formatSchemaEngineError(logs, e.stderr)}`)
}
} else {
throw new Error(`Schema engine exited. ${_e}`)
Expand All @@ -158,7 +173,7 @@ export async function dropDatabase(connectionString: string, cwd = process.cwd()
if (e.stderr) {
const logs = parseJsonFromStderr(e.stderr)

throw new Error(`Schema engine error:\n${logs.map((log) => log.fields.message).join('\n')}`)
throw new Error(`Schema engine error:\n${formatSchemaEngineError(logs, String(e.stderr))}`)
} else {
throw new Error(`Schema engine exited. ${e}`)
}
Expand Down