본문으로 건너뛰기
cccskills
무료GitHub에 공개

ci-stable-log-testing

CI環境で安定動作するPythonテスト(特にstructlogのログ検証)を書く際に使用。「CIで失敗する」「capture_logsが不安定」「ログアサーションがCIだけ落ちる」「caplogフォールバック」「ANSI混入」「xdistでログが取れない」という課題で起動。

capture_logs→caplogフォールバック→ANSI正規化→アサーション設計の流れで堅牢なテストを書く。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구GitHub
설치 방법 보기

포함된 파일(3)

  • SKILL.md6.0 KB
  • references/troubleshooting.md2.6 KB
  • scripts/find_event_logs.py5.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

CI-Stable Test: structlogログ検証の安定化パターン

背景と問題

structlog.testing.capture_logs() はローカルでは動作するが、CI環境(特にpytest-xdist並列実行、GitHub Actions ubuntu runner)では以下の理由でログが取得できないことがある。

  1. モジュールレベルのロガーキャッシュ: 先行テストがロガーをバインド済みだと capture_logs() のコンテキストマネージャがログを捕捉できない
  2. pytest-xdist並列実行: ワーカー間でstructlogのグローバル設定が競合する
  3. caplogメッセージのANSI混入: CI環境のstructlogレンダラがANSIエスケープ付きで出力し、key=value の単純一致が崩れる
  4. exc_info の表現差異: CI環境では record.exc_info が None でもメッセージ本文にTracebackが含まれる場合がある

解決パターン: 2段階フォールバック

パターン概要

capture_logs() で構造化ログを取得
 ├── 取得成功 → 構造化dictで厳密アサーション
 └── 取得失敗(空リスト)→ caplog にフォールバック
      └── ANSIエスケープ除去 + key=value正規化してアサーション

実装手順

Step 1: ANSI正規化ユーティリティを定義

import re

_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")

def _normalize_log_message(message: str) -> str:
    """ANSIエスケープを除去し、key=value比較しやすい形へ正規化する。"""
    normalized = _ANSI_ESCAPE_RE.sub("", message)
    normalized = re.sub(r"\s*=\s*", "=", normalized)
    return normalized

Step 2: フォールバック付きログ抽出ヘルパーを定義

def _find_event_logs(
    captured: list[dict[str, object]],
    *,
    event: str,
    caplog: pytest.LogCaptureFixture,
) -> list[dict[str, object]]:
    """capture_logs の結果が空の場合に caplog をフォールバック利用する。"""
    structured = [e for e in captured if e.get("event") == event]
    if structured:
        return structured

    fallback: list[dict[str, object]] = []
    seen: set[str] = set()
    for record in caplog.records:
        msg = _normalize_log_message(record.getMessage())
        raw = _normalize_log_message(str(record.msg))
        if event in msg or event in raw:
            merged = msg if event in msg else raw
            if merged in seen:
                continue
            seen.add(merged)
            fallback.append({
                "event": event,
                "log_level": record.levelname.lower(),
                "message": merged,
            })
    return fallback

設計判断:

  • seen セットで重複メッセージを除外(capture_logsとcaplog両方に同じログが流れるケースへの対策)
  • record.msg(フォーマット前)も確認する(ハンドラチェーンによる差異を吸収)

Step 3: テスト関数でcaplogを併用

@pytest.mark.asyncio
async def test_example(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
    caplog.clear()
    caplog.set_level(logging.WARNING)  # 対象ログレベルに合わせる
    with capture_logs() as cap:
        await some_function()

    logs = _find_event_logs(cap, event="my_event", caplog=caplog)
    assert len(logs) >= 1

Step 4: アサーションの分岐パターン

log_entry = logs[0]
assert log_entry["log_level"] == "warning"

# 構造化パス(capture_logs成功時)はキーで直接検証
if "detail_key" in log_entry:
    assert log_entry["detail_key"] == expected_value
else:
    # フォールバックパス: 正規化済みメッセージでkey=value検証
    assert "detail_key=expected_value" in log_entry["message"]

判断チェックリスト

テストでstructlogのログ検証を書く際、以下に該当すれば本パターンを適用する。

  • structlog.testing.capture_logs() でログを検証している
  • CI(GitHub Actions等)でのみテストが失敗する
  • pytest-xdist(-n auto)で並列実行している
  • ログメッセージの key=value 部分文字列でアサーションしている

exc_info の検証

CI環境では record.exc_info が None でもメッセージ本文にTracebackが埋め込まれることがある。

fallback.append({
    # ...
    "exc_info": (
        record.exc_info is not None
        or "Traceback (most recent call last):" in merged
    ),
})

件数アサーションの注意

CI環境ではハンドラチェーンの差異によりログが複数回記録されることがある。

  • == 1 よりも >= 1 を推奨(特にフォールバックパスで)
  • 複数件を許容する場合は logs[-1](最後のエントリ)で検証すると安定する
  • seen セットによる重複除外を入れても、formatter差異で完全一致しない場合がある

やらないこと(スコープ外)

  • ログの出力順序の厳密な検証(CI環境では保証されない)
  • structlog自体の設定変更やモンキーパッチ
  • caplogだけに依存する方式(構造化ログのキーアクセスが失われる)
  • プロダクションコードの変更(テスト側のみで吸収する)

参照

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

stacked PRの下位PRがマージされた後に上位branchをrestack(rebase/cherry-pick再構成)する際に使用。「restackして」「上位PRを更新して」「restack after merge」「PRのbaseを更新」「スタックを整理」という依頼でトリガーする。対象sliceの特定→rebase→変更ファイル確認と代表テスト実行→force-with-leaseでpush→PR base更新→auto-merge判定の順で進める。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

cc-sdd実装用統合ブランチのコミット群(ベースブランチとの差分)をreview slice単位のstacked PRに再構成する際に使用。「review sliceに分割」「stacked PRを作成」「PRをスタックして」「レビュー用にPRを分けて」「review slice stack」という依頼でトリガーする。dry-run(既定)でslice計画を提示し、apply指示で実際にブランチ・PRを作成する。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

GitHub Issueを作成してGitHubに投稿するスキル。「Issueを作成」「Issue作成」「Issueにして」「Issueとして投稿」「GitHubにIssue」「create issue」「open issue」「バグ報告」「機能追加をIssueに」「改善提案をIssueに」などの依頼でトリガーする。コンテキスト収集→Issue本文生成→ラベル存在確認・作成→gh issue create投稿の流れでIssueを作成する。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

Dockerベースのアプリケーションのデプロイメント手順書(Markdown)を作成するスキル。「デプロイ手順書」「デプロイメント手順」「deployment guide」「本番環境への配置手順」「サーバーへの配置手順」などの依頼でトリガーする。Docker Composeビルド→イメージ転送→サーバー配置→設定→動作確認→cron設定の流れで手順書を生成する。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

タスク完了後にPCを休止状態にする。Use when: ユーザーが「休止」「hibernate」「タスク完了後に休止状態」「作業後にPCをスリープ/休止」と明示的に指示した場合のみ。正常終了時だけでなく、エラーや未解決事項が残っていても、明示指示があれば最後に休止へ移行する。Windows only.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

Investigate implementation failures using root-cause-first debugging. Use when an implementer is blocked, verification fails, or repeated remediation does not converge.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

studiogadget/skills32026년 5월 25일 업데이트

studiogadget의 스킬 모두 보기

이 스킬의 문제 신고