Path: blob/main/tests/smoke/logging/log-level-and-formats.test.ts
12921 views
/*1* log-level-direct.test.ts2*3* Copyright (C) 2020-2025 Posit Software, PBC4*5*/67import { basename, extname } from "../../../src/deno_ral/path.ts";8import { execProcess } from "../../../src/core/process.ts";9import { md5HashSync } from "../../../src/core/hash.ts";10import { safeRemoveIfExists } from "../../../src/core/path.ts";11import { quartoDevCmd, outputForInput } from "../../utils.ts";12import { assert } from "testing/asserts";13import { LogFormat } from "../../../src/core/log.ts";14import { existsSync } from "../../../src/deno_ral/fs.ts";15import { readExecuteOutput } from "../../test.ts";1617// Simple minimal document for testing18const testDoc = "docs/minimal.qmd";19const testDocWithError = "docs/logging/lua-error.qmd";2021// NOTE: We intentionally do NOT test environment variables (QUARTO_LOG, QUARTO_LOG_LEVEL, QUARTO_LOG_FORMAT)22// because they can interfere with other tests running concurrently in the Deno environment.23// Instead, we focus on testing the CLI arguments which are isolated to each test.2425// Those tests are not using testQuartoCmd because they are testing the log level directly26// and custom helpers in tests() are modifying the logging configuration.27// This is why we use Deno.test directly here.2829function testLogDirectly(options: {30testName: string,31level: string,32format?: LogFormat,33logFile?: string,34fileToRender?: string,35quiet?: boolean,36expectedOutputs?: {37// To test log output for document with errors38shouldSucceed?: boolean39// For plain format, we can check for specific text in the output40shouldContain?: string[],41shouldNotContain?: string[],42// For JSON format, we can also check for specific log levels43shouldContainLevel?: string[],44shouldNotContainLevel?: string[],45}46}) {47Deno.test({48name: options.testName,49fn: async () => {50// Generate a unique log file path if one was provided51// This is to avoid conflicts when running multiple tests in parallel52let logFile = options.logFile;53if (logFile) {54const testNameHash = md5HashSync(options.testName);55const extension = extname(logFile);56const baseName = basename(logFile, extension);57logFile = `${baseName}-${testNameHash}${extension}`;58}5960try {61// Build command args62const args = ["render"];6364// Add file path (default to testDoc if not specified)65if (!options.fileToRender) {66options.fileToRender = testDoc;67}68args.push(options.fileToRender);6970// Add log level71args.push("--log-level", options.level);7273// Add format if specified74if (options.format) {75args.push("--log-format", options.format);76}7778if (logFile) {79args.push("--log", logFile);80}8182// Add quiet if specified83if (options.quiet) {84args.push("--quiet");85}8687// Execute quarto directly88const process = await execProcess({89cmd: quartoDevCmd(),90args: args,91stdout: "piped",92stderr: "piped"93});9495// Get stdout/stderr with fallback to empty string96const stdout = process.stdout || "";97const stderr = process.stderr || "";98const allOutput = stdout + stderr;99100// Check success/failure expectation101if (options.expectedOutputs?.shouldSucceed !== undefined) {102assert(103process.success === options.expectedOutputs.shouldSucceed,104options.expectedOutputs.shouldSucceed105? `Process unexpectedly failed: ${stderr}`106: `Process unexpectedly succeeded (expected failure)`107);108}109110// Check for expected content111if (options.expectedOutputs?.shouldContain) {112for (const text of options.expectedOutputs.shouldContain) {113assert(114allOutput.includes(text),115`Output should contain '${text}' but didn't.\nOutput: ${allOutput}`116);117}118}119120// Check for unwanted content121if (options.expectedOutputs?.shouldNotContain) {122for (const text of options.expectedOutputs.shouldNotContain) {123assert(124!allOutput.includes(text),125`Output shouldn't contain '${text}' but did.\nOutput: ${allOutput}`126);127}128}129130// For quiet mode, verify no output131if (options.quiet) {132assert(133stdout.trim() === "",134`Expected no stdout with --quiet option, but found: ${stdout}`135);136}137138139// If JSON format is specified, verify the output is valid JSON140if (logFile && options.format === "json-stream") {141assert(existsSync(logFile), "Log file should exist");142let foundValidJson = false;143try {144const outputs = readExecuteOutput(logFile);145foundValidJson = true;146outputs.filter((out) => out.msg !== "" && options.expectedOutputs?.shouldNotContainLevel?.includes(out.levelName)).forEach(147(out) => {148assert(false, `JSON output should not contain level ${out.levelName}, but found: ${out.msg}`);149}150);151outputs.filter((out) => out.msg !== "" && options.expectedOutputs?.shouldContainLevel?.includes(out.levelName)).forEach(152(out) => {153let json = undefined;154try {155json = JSON.parse(out.msg);156} catch {157assert(false, "Error parsing JSON returned by quarto meta");158}159assert(160Object.keys(json).length > 0,161"JSON returned by quarto meta seems invalid",162);163}164);165166} catch (e) {}167assert(foundValidJson, "JSON format should produce valid JSON output");168}169} finally {170// Clean up log file if it exists171if (logFile) {172safeRemoveIfExists(logFile);173}174// Clean up any rendered output files175if (options.fileToRender) {176const output = outputForInput(options.fileToRender, 'html');177safeRemoveIfExists(output.outputPath);178safeRemoveIfExists(output.supportPath);179}180}181}182});183};184185// This will always be shown in debug output as we'll show pandoc call186const debugHintText = "pandoc --verbose --trace";187// This will be shown in info output188const infoHintText = function(testDoc: string) {189return `Output created: ${basename(testDoc, extname(testDoc))}.html`;190};191192testLogDirectly({193testName: "Plain format - DEBUG level should show all log messages",194level: "debug",195format: "plain",196fileToRender: testDoc,197expectedOutputs: {198shouldContain: [debugHintText, infoHintText(testDoc)],199shouldSucceed: true200}201});202203testLogDirectly({204testName: "Plain format - INFO level should not show DEBUG messages",205level: "info",206format: "plain",207fileToRender: testDoc,208expectedOutputs: {209shouldContain: [infoHintText(testDoc)],210shouldNotContain: [debugHintText],211shouldSucceed: true212}213});214215testLogDirectly({216testName: "Plain format - WARN level should not show INFO or DEBUG messages",217level: "warn",218format: "plain",219fileToRender: testDocWithError,220expectedOutputs: {221shouldContain: ["WARN:", "ERROR:"],222shouldNotContain: [debugHintText, infoHintText(testDocWithError)],223shouldSucceed: false224}225});226227testLogDirectly({228testName: "Plain format - ERROR level should only show ERROR messages",229level: "error",230format: "plain",231fileToRender: testDocWithError,232expectedOutputs: {233shouldContain: ["ERROR:"],234shouldNotContain: [debugHintText, infoHintText(testDocWithError), "WARN:"],235shouldSucceed: false236}237});238239testLogDirectly({240testName: "Json-Stream format - should produce parseable JSON in a log file with INFO level",241level: "info",242format: "json-stream",243logFile: "test-log.json",244expectedOutputs: {245shouldSucceed: true,246shouldContainLevel: ["INFO"],247shouldNotContainLevel: ["DEBUG"]248}249});250251testLogDirectly({252testName: "Json-Stream format - should produce parseable JSON in a log file with DEBUG level",253level: "debug",254format: "json-stream",255logFile: "test-log.json",256expectedOutputs: {257shouldSucceed: true,258shouldContainLevel: ["DEBUG", "INFO"],259}260});261262testLogDirectly({263testName: "Json-Stream format - should produce parseable JSON in a log file with WARN level",264level: "warn",265format: "json-stream",266logFile: "test-log.json",267fileToRender: testDocWithError,268expectedOutputs: {269shouldSucceed: false,270shouldContainLevel: ["WARN", "ERROR"],271shouldNotContainLevel: ["INFO", "DEBUG"],272}273});274275testLogDirectly({276testName: "Json-Stream format - should produce parseable JSON in a log file with ERROR level",277level: "error",278format: "json-stream",279logFile: "test-log.json",280fileToRender: testDocWithError,281expectedOutputs: {282shouldSucceed: false,283shouldContainLevel: ["ERROR"],284shouldNotContainLevel: ["DEBUG", "INFO", "WARN"]285}286});287288289// 6. Testing quiet mode290testLogDirectly({291testName: "Quiet mode should suppress all console output",292level: "debug",293format: "plain",294quiet: true,295expectedOutputs: {296shouldSucceed: true297}298});299300testLogDirectly({301testName: "Quiet mode should not suppress all output in JSON log file",302level: "debug",303format: "json-stream",304logFile: "test-log.json",305quiet: true,306expectedOutputs: {307shouldSucceed: true,308shouldContainLevel: ["DEBUG", "INFO"],309}310});311312// 7. Testing quiet mode with errors313testLogDirectly({314testName: "Plain format - Quiet mode should suppress output even with errors",315level: "debug",316format: "plain",317quiet: true,318fileToRender: testDocWithError,319expectedOutputs: {320shouldSucceed: false321}322});323324testLogDirectly({325testName: "Json-Stream - Quiet mode should not suppress output even with errors",326level: "debug",327format: "json-stream",328logFile: "test-log.json",329quiet: true,330fileToRender: testDocWithError,331expectedOutputs: {332shouldSucceed: false,333shouldContainLevel: ["DEBUG", "INFO", "WARN", "ERROR"]334}335});336337338testLogDirectly({339testName: "Log level should be case-insensitive (INFO vs info)",340level: "INFO",341format: "plain",342fileToRender: testDoc,343expectedOutputs: {344shouldContain: [infoHintText(testDoc)],345shouldNotContain: [debugHintText],346shouldSucceed: true347}348});349350testLogDirectly({351testName: "WARNING should be equivalent to WARN level",352level: "WARNING",353format: "plain",354fileToRender: testDocWithError,355expectedOutputs: {356shouldContain: ["WARN:", "ERROR:"],357shouldNotContain: [debugHintText, infoHintText(testDocWithError)],358shouldSucceed: false359}360});361362363364365366