PHPUnitのカバレッジバッジを作成する

# バッジを表示

https://gist.github.com/hellowork-mhlw/0a5d2e3a3121aefaf01a48b2b53c9ef3

```markdown
![](https://coverage.deno.dev/hoge.svg)
```

# バッジを更新
最後の数字を変更することで更新できます。`/hoge.svg`などのパスは好きなのいれてください
https://coverage.deno.dev/hoge.svg?97

# CI/CDに組み込む場合

```bash
vendor/bin/phpunit --coverage-html reports
# for Laravel
# php artisan test --coverage-html reports

percentage=$(printf %.0f $(grep -Pom1 '(?<=aria-valuenow=").*?(?=")' reports/index.html))
curl https://coverage.deno.dev/hoge.svg?$percentage
```



# ソースコード

https://dash.deno.com/_api/projects/ のAPIで環境変数を更新できます。
```typescript
import { serve } from "https://deno.land/std@0.155.0/http/server.ts";

const seeds = {
    width: [107, 3, 4],
    cwidth: [29, 3, 4],
    x: [915, 15, 20],
    textLength: [190, 30, 40]
}
const calc = (p, s) => [s[0], s[1]*2, s[1]*2].slice(0, p.length).reduce((a, b) => a+b)
const thresholds = [97, 93, 80, 65, 45, 15, 0];
const colors = [1217, 9947648, 10790429, 14660375, 16678199, 14703940, 10461087].map(i => i.toString(16));

function generateBadge(p) {
  const width = calc(p, seeds.width)
  const cwidth = calc(p, seeds.cwidth)
  const x = calc(p, seeds.x)
  const textLength = calc(p, seeds.textLength)
  const color = colors[thresholds.findIndex(i => p >= i)]

  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="20" role="img" aria-label="coverage: ${p}%"><title>coverage: ${p}%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="${width}" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="78" height="20" fill="#555"/><rect x="78" width="${cwidth}" height="20" fill="#${color}"/><rect width="${width}" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0SHViPC90aXRsZT48cGF0aCBkPSJNMTIgLjI5N2MtNi42MyAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDMgMy40MzggOS44IDguMjA1IDExLjM4NS42LjExMy44Mi0uMjU4LjgyLS41NzcgMC0uMjg1LS4wMS0xLjA0LS4wMTUtMi4wNC0zLjMzOC43MjQtNC4wNDItMS42MS00LjA0Mi0xLjYxQzQuNDIyIDE4LjA3IDMuNjMzIDE3LjcgMy42MzMgMTcuN2MtMS4wODctLjc0NC4wODQtLjcyOS4wODQtLjcyOSAxLjIwNS4wODQgMS44MzggMS4yMzYgMS44MzggMS4yMzYgMS4wNyAxLjgzNSAyLjgwOSAxLjMwNSAzLjQ5NS45OTguMTA4LS43NzYuNDE3LTEuMzA1Ljc2LTEuNjA1LTIuNjY1LS4zLTUuNDY2LTEuMzMyLTUuNDY2LTUuOTMgMC0xLjMxLjQ2NS0yLjM4IDEuMjM1LTMuMjItLjEzNS0uMzAzLS41NC0xLjUyMy4xMDUtMy4xNzYgMCAwIDEuMDA1LS4zMjIgMy4zIDEuMjMuOTYtLjI2NyAxLjk4LS4zOTkgMy0uNDA1IDEuMDIuMDA2IDIuMDQuMTM4IDMgLjQwNSAyLjI4LTEuNTUyIDMuMjg1LTEuMjMgMy4yODUtMS4yMy42NDUgMS42NTMuMjQgMi44NzMuMTIgMy4xNzYuNzY1Ljg0IDEuMjMgMS45MSAxLjIzIDMuMjIgMCA0LjYxLTIuODA1IDUuNjI1LTUuNDc1IDUuOTIuNDIuMzYuODEgMS4wOTYuODEgMi4yMiAwIDEuNjA2LS4wMTUgMi44OTYtLjAxNSAzLjI4NiAwIC4zMTUuMjEuNjkuODI1LjU3QzIwLjU2NSAyMi4wOTIgMjQgMTcuNTkyIDI0IDEyLjI5N2MwLTYuNjI3LTUuMzczLTEyLTEyLTEyIi8+PC9zdmc+"/><text aria-hidden="true" x="485" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="485" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="${x}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${textLength}">${p}%</text><text x="${x}" y="140" transform="scale(.1)" fill="#fff" textLength="${textLength}">${p}%</text></g></svg>`
}

serve(async (req: Request) => {
  const url = new URL(req.url)
  const p = url.search.slice(1) || Deno.env.get(url.pathname) || '0'
  if (url.search) {
    fetch('https://dash.deno.com/_api/projects/coverage/env', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'cookie': 'token=' + ACCESS_TOKEN
      },
      body: JSON.stringify({[url.pathname]: p})
    })
  }
  return new Response(generateBadge(p), {
    headers: {
      'content-type': 'image/svg+xml; charset=utf-8',
      'cache-control': 'private, no-store, no-cache, must-revalidate',
      'pragma': 'no-cache'
    },
  });
});
```