Azure DevOps の Pipeline で動かす cucumberからエラーログをとったメモ

# 概要
Azure DevOpsのパイプラインの上でE2Eテストをしたかった。
[前回](https://qiita.com/hibohiboo/items/24fcb7f729a4edcfcf2a)、認証をパイプラインで動かすのに失敗したので、CI環境では認証はスキップするつくりにした。
それでもなかなか動かなかったので、試行錯誤した記録を残す。

[この時点のソース](https://github.com/hibohiboo/odyssage/blob/20a768a1ecdb5ffcb58c9fa6b9fe698ec26a20d1/packages/bdd-e2e-test/azure-pipeline.yml)

# ソースコード

```yml:packages/bdd-e2e-test/azure-pipeline.yml
trigger:
  branches:
    include:
      - id/75/bddOnPipeline

jobs:
  - job: bdd_test
    displayName: 'Run BDD E2E tests'
    pool:
      vmImage: 'ubuntu-latest'

    services:
      postgres:
        image: postgres:17.4-alpine
        ports:
          - 5432:5432
        env:
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: main
        options: >-
          --health-cmd="pg_isready"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - checkout: self

      - task: UseNode@1
        inputs:
          version: '22.x'

      - script: |
          curl -fsSL https://bun.sh/install | bash
        displayName: 'Install Bun'

      - script: echo "##vso[task.setvariable variable=PATH]$(HOME)/.bun/bin:$(PATH)"
        displayName: 'Add Bun to PATH'

      - script: bun install
        displayName: 'Install Dependencies'

      - script: npx playwright install --with-deps chromium
        workingDirectory: apps/frontend
        displayName: 'download new browsers'

      - script: cp .dev.vars.ci .dev.vars
        workingDirectory: apps/backend
        displayName: 'Setting Backend env vars'

      - script: cp .env.sample .dev.local
        workingDirectory: apps/frontend
        displayName: 'Setting Frontend env vars'

      - script: |
          bun run dev > backend.log 2>&1 &
          echo $! > ../backend.pid
        workingDirectory: apps/backend
        displayName: 'Start Backend'

      # ホットリロードなど不要なので、bun run dev ではなく bun run preview を使う
      - script: |
          bun run preview:ci > frontend.log 2>&1 &
          echo $! > ../frontend.pid
        workingDirectory: apps/frontend
        displayName: 'Start Frontend'

      - script: |
          echo "Waiting for frontend server..."
          for i in {1..20}; do
            if curl -s http://localhost:5173 > /dev/null; then
              echo "Frontend server is up."
              break
            fi
            sleep 2
          done

          echo "Waiting for backend server..."
          for i in {1..20}; do
            if curl -s http://localhost:8787 > /dev/null; then
              echo "Backend server is up."
              break
            fi
            sleep 2
          done
        displayName: 'Wait for servers to start'

      - script: bun run migrate:azci
        workingDirectory: packages/database
        displayName: 'Migration RDB'

      - script: bun run bdd-test
        workingDirectory: packages/bdd-e2e-test
        displayName: 'Run E2E Tests'
        env:
          CI: true

      # 調査用にE2Eテストのスクリーンショットやcontentsを確認できるようにする
      - task: PublishBuildArtifacts@1
        condition: always()
        inputs:
          pathToPublish: 'packages/bdd-e2e-test/output'
          artifactName: 'e2e-output'
        displayName: 'Upload screenshots as artifacts'

      # 調査用にバックエンドのログを確認できるようにする
      - task: PublishBuildArtifacts@1
        condition: always()
        inputs:
          pathToPublish: 'apps/backend/backend.log'
          artifactName: 'backend-log'
        displayName: 'Upload screenshots as artifacts'

      - script: |
          kill $(cat backend.pid)
          kill $(cat frontend.pid)
        displayName: 'Cleanup Servers'
        workingDirectory: apps
        condition: always()
```



# 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d1181f1b-9582-44c1-b4ec-b8625875515c.png)


# 調査記録
## バックエンドのログ
PublishBuildArtifactsを使う。

```diff
      - script: |
+          bun run dev > backend.log 2>&1 &
          echo $! > ../backend.pid
        workingDirectory: apps/backend
        displayName: 'Start Backend'
```

```diff
      # 調査用にバックエンドのログを確認できるようにする
      - task: PublishBuildArtifacts@1
        condition: always()
        inputs:
+         pathToPublish: 'apps/backend/backend.log'
          artifactName: 'backend-log'
        displayName: 'Upload screenshots as artifacts
```

実行結果の
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/c75f2b3b-73f8-40ff-9e07-fb8c2ed50738.png)

Publishをクリックすると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4457264d-069f-434c-816b-a45e42726a34.png)

出力されたログを確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7107ddc3-6b01-4ee2-a256-82bbd36cb001.png)


## フロントエンド
`DEBUG=pw:api `でデバッグログを確認できる

```json:packages/bdd-e2e-test/package.json
    "bdd-test": "DEBUG=pw:api cucumber-js --config cucumber.mjs --exit"
```

ヘッドレスブラウザのログは下記のように`Before`でイベントをひろう。

```ts:packages/bdd-e2e-test/e2e/step-definitions/scenario.steps.ts
Before(async function (this) {
  const browser = await chromium.launch({
    headless: process.env.CI === 'true',
  }); // headless: true にするとブラウザが表示されない
  const context = await browser.newContext();
  this.page = await context.newPage();

  // ヘッドレスブラウザ―のコンソール出力をキャッチする
  this.page.on('console', (msg: any) => {
    if (msg.type() === 'error') {
      console.error(`[Browser Console Error]: ${msg.text()}`);
    } else if (msg.type() === 'warning') {
      console.warn(`[Browser Console Warning]: ${msg.text()}`);
    } else {
      console.log(`[Browser Console]: ${msg.text()}`);
    }
  });
  this.page.on('request', (request: any) =>
    console.log(`Request: ${request.method()} ${request.url()}`),
  );
  this.page.on('response', (response: any) => {
    console.log(`Response: ${response.status()} ${response.url()}`);
    if (response.status() >= 400) {
      console.error(`Error Response: ${response.status()} ${response.url()}`);
    }
  });
});
```

スクリーンショットを取ったり、`page.content`でHTMLを出力したりで調査できる。

```ts:packages/bdd-e2e-test/e2e/step-definitions/scenario.steps.ts
When(
  'ユーザーが「 {string} 」リンクをクリックする',
  async function (this, text) {
    const { page } = this;

    await page.screenshot({
      path: 'output/screenshots/debug.png',
      fullPage: true,
    });
    const content = await page.content();
    fs.writeFileSync('output/debug.html', content);
    await page.waitForLoadState('networkidle');
    await page.waitForTimeout(500);
    await page.screenshot({
      path: 'output/screenshots/after_networkidle.png',
      fullPage: true,
    });
    fs.writeFileSync('output/after_networkidle.html', await page.content());

    await page.waitForSelector(`a:has-text("${text}")`);
    await page.getByRole('link', { name: text }).nth(0).click();
  },
);
```

# 参考

[Greenプルリク](https://github.com/hibohiboo/odyssage/pull/76/files)

# 余談

エラー検証中に下記のエラーがなかなか難しかった。
dbの接続を使いまわしていたら出たエラーだった。[修正こみ](https://github.com/hibohiboo/odyssage/pull/76/commits/a10d0fd234c18e530e06642d55da14f7375f8d3d)
```
Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance. (I/O type: Writable)
```

# 追記： Neon Proxyのログ

```yaml
      - script: |
          # ログファイルパスを定義
          LOG_DIR=$(pwd)/neon-proxy-logs
          mkdir -p $LOG_DIR

          echo "Starting Neon Proxy..."
          docker run -d --name neon-proxy \
            -e PG_CONNECTION_STRING=postgres://postgres:postgres@host.docker.internal:5432/main \
            -p 4444:4444 \
            --add-host=host.docker.internal:host-gateway \
            ghcr.io/timowilhelm/local-neon-http-proxy:main

          # バックグラウンドでログ収集を開始
          docker logs -f neon-proxy > $LOG_DIR/neon-proxy.log 2>&1 &
          LOGGER_PID=$!
          echo $LOGGER_PID > $LOG_DIR/neon.pid

          # Proxy が立ち上がるまで待機
          echo "Waiting for Neon Proxy..."
          for i in {1..20}; do
            if curl -s http://db.localtest.me:4444/sql &>/dev/null; then
              echo "Neon Proxy is ready"
              break
            fi
            sleep 2
          done
        displayName: 'Start Neon Proxy'
      # Neon Proxyログをアーティファクトとして保存するためのタスク
      - task: PublishBuildArtifacts@1
        condition: always()
        inputs:
          pathToPublish: 'neon-proxy-logs'
          artifactName: 'neon-proxy-logs'
        displayName: 'Upload Neon Proxy logs as artifacts'

      - script: kill $(cat neon.pid)
        displayName: 'Cleanup Neon Proxy logs'
        workingDirectory: neon-proxy-logs
        condition: always()
```

[ソースコード](https://github.com/hibohiboo/odyssage/blob/754243ac79a2368cd32fb5500385dd6eeab0b326/packages/bdd-e2e-test/azure-pipeline.yml)