You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Golang Docker API无法获取单元测试日志的问题与存储方案

Golang调用Docker API无法获取Python unittest日志问题解决

问题描述

用Golang调用Docker API时,能正常获取容器内fmt.Print这类标准输出日志,但抓不到Python unittest生成的日志,同时需要把unittest日志存到变量里。

问题原因

  • Python的unittest模块默认把日志输出到标准错误流(stderr),而原代码调用ContainerLogs时只开启了ShowStdout: true,没捕获stderr,所以看不到unittest的输出。
  • Docker容器环境属于非交互式终端,部分输出可能有缓冲,但核心问题还是stderr未被采集。

解决步骤

1. 同时捕获stdout和stderr

修改ContainerLogs的参数,同时开启ShowStdout和ShowStderr,这样就能拿到unittest的日志。

2. 将日志存储到变量

用bytes.Buffer替代os.Stdout/os.Stderr来接收日志内容,之后可以把缓冲里的内容转为字符串,存到变量中复用。

3. 可选:调整unittest输出行为(非必需)

如果想让unittest输出到stdout,可以在测试代码里修改unittest.main()的参数,比如:

if __name__ == '__main__':
    import sys
    unittest.main(stream=sys.stdout)

但更推荐直接捕获stderr,这是unittest的标准行为,不需要改动Python代码。

修改后的完整代码

package main

import (
    "bytes"
    "context"
    "fmt"
    "os"
    "path/filepath"

    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/stdcopy"
)

func main() {
    userCode := `
def two_sum(nums, target):
    num_dict = {}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in num_dict:
            return [num_dict[complement], i]
        num_dict[num] = i
    return []
    `

    // Predefined test cases
    testCode := `
import unittest
from main import two_sum

class TestTwoSum(unittest.TestCase):
    def test_case_1(self):
        self.assertEqual(two_sum([2, 7, 11, 15], 9), [0, 1])

    def test_case_2(self):
        self.assertEqual(two_sum([3, 2, 4], 6), [1, 2])

if __name__ == '__main__':
    unittest.main()
`
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        panic(err)
    }
    defer cli.Close()

    // 1. Create 2 temp files
    hostWorkingDir, err := os.MkdirTemp("", "")
    check(err)
    fmt.Println("Temp dir name:", hostWorkingDir)
    defer os.RemoveAll(hostWorkingDir)

    codeFilePath := filepath.Join(hostWorkingDir, "main.py")
    codeFile, err := os.Create(codeFilePath)
    if err != nil {
        fmt.Println("failed to create source file", err)
        return
    }
    fmt.Println("Code file name:", codeFile.Name())
    defer codeFile.Close()

    testFilePath := filepath.Join(hostWorkingDir, "test.py")
    testFile, err := os.Create(testFilePath)
    if err != nil {
        fmt.Println("failed to create test file", err)
        return
    }
    fmt.Println("Test file name:", testFile.Name())
    defer testFile.Close()

    // 2. Write code into 2 temp files:
    _, err = codeFile.WriteString(userCode)
    if err != nil {
        fmt.Print("failed to write source file")
        return
    }

    _, err = testFile.WriteString(testCode)
    if err != nil {
        fmt.Print("failed to write test file")
        return
    }

    // 3. Create Container
    ctx := context.Background()

    workingDir := "/work"
    programFileDirectory, codePath := filepath.Split(codeFile.Name())
    _, testPath := filepath.Split(testFile.Name())
    codePath = filepath.Join(workingDir, codePath)
    fmt.Println(codePath)
    testPath = filepath.Join(workingDir, testPath)
    fmt.Println(testPath)
    resp, err := cli.ContainerCreate(ctx, &container.Config{
        Image:        "docker.io/library/python:3.13-rc-slim",
        Cmd:          []string{"timeout", "--foreground", "10s", "python", "test.py"},
        WorkingDir:   workingDir,
        AttachStdin:  true,
        AttachStdout: true,
        AttachStderr: true,
        OpenStdin:    true,
        StdinOnce:    true,
    }, &container.HostConfig{
        Binds: []string{fmt.Sprintf("%s:%s", programFileDirectory, workingDir)},
        Resources: container.Resources{
            CPUQuota: 1000000,
            Memory:   int64(256 * 1024 * 1024), // 256 MB
        },
        NetworkMode: "none"}, nil, nil, "")

    if err != nil {
        panic(err)
    }

    //4. Start Container
    if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
        panic(err)
    }

    //5. Wait for container to response
    statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
    select {
    case err := <-errCh:
        if err != nil {
            panic(err)
        }
    case <-statusCh:
    }

    // 关键修改1:同时开启ShowStdout和ShowStderr
    out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{
        ShowStdout: true,
        ShowStderr: true,
    })
    if err != nil {
        panic(err)
    }

    // 关键修改2:用Buffer接收日志,存储到变量
    var stdoutBuf, stderrBuf bytes.Buffer
    _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out)
    if err != nil {
        panic(err)
    }

    // 输出查看结果
    fmt.Println("=== 标准输出内容 ===")
    fmt.Println(stdoutBuf.String())
    fmt.Println("=== 标准错误内容(unittest日志) ===")
    fmt.Println(stderrBuf.String())

    // 把日志存储到变量,后续可按需使用
    allLogs := stdoutBuf.String() + "\n" + stderrBuf.String()
    fmt.Println("=== 全部日志内容 ===")
    fmt.Println(allLogs)
}

func check(e error) {
    if e != nil {
        panic(e)
    }
}

关键修改说明

  1. 日志捕获参数:在container.LogsOptions中新增ShowStderr: true,确保采集到unittest输出的stderr内容。
  2. 日志存储:使用bytes.Buffer替代控制台输出,将stdout和stderr分别存入缓冲,之后可以转为字符串变量保存或处理。

内容的提问来源于stack exchange,提问作者Triet Dao

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 01:54:54