Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
# Naming any permission sets every one not listed to none, so the checkout's
# read is spelled out beside the OIDC token the Codecov upload signs with.
permissions:
contents: read
id-token: write
steps:
# Full history: the commit gate walks the range this push or PR adds, and
# a shallow clone has no base to compare against.
Expand Down Expand Up @@ -171,8 +176,29 @@ jobs:
# The ETag-cache tests run SQLite on the host via sqflite_common_ffi;
# ubuntu ships libsqlite3.so.0 but not the unversioned symlink its loader
# needs, so install the dev package.
- name: Install SQLite (sqflite_common_ffi host tests)
- name: Install SQLite
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev

- name: Test
run: bash tool/dev/test.sh
# The test suite, run once, with coverage. The coverage run *is* the test
# run — a failing test fails this step exactly as tool/dev/test.sh would —
# so the lcov below costs a few seconds of instrumentation rather than a
# second pass over the suite.
- name: Test (with coverage)
run: bash tool/dev/coverage.sh

# Codecov is where the badge in the README, its sunburst graph and the
# pull-request comment all come from; codecov.yml holds the settings.
#
# OIDC rather than a token: there is nothing to store and nothing to
# rotate. A pull request from a fork cannot mint one, so the upload is
# allowed to fail rather than fail the run — by this point the suite has
# passed, which is what the check is for. Uploads from forks need the
# tokenless opt-out for public repositories, in the Codecov org settings.
- name: Upload coverage to Codecov
if: ${{ !cancelled() && hashFiles('coverage/lcov.info') != '' }}
uses: codecov/codecov-action@v5
with:
use_oidc: true
files: coverage/lcov.info
disable_search: true
fail_ci_if_error: false
35 changes: 35 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tool/dev/analyze.sh
|---|---|
| Start the app | `tool/run.sh` (see [Running](#running)) |
| Run the tests | `tool/dev/test.sh` |
| Test coverage, into `coverage/` | `tool/dev/coverage.sh` (see [Coverage](#coverage)) |
| Format + analyze | `tool/dev/analyze.sh` |
| Reformat in place | `tool/dev/format.sh` |
| Resolve dependencies | `tool/dev/deps.sh` (`--offline` when pub.dev stalls) |
Expand Down Expand Up @@ -125,6 +126,40 @@ a target Dart file and fails with `Target file "ios" not found`.
open "$(xcode-select -p)/../Applications/DeviceHub.app"
```

## Coverage

```sh
tool/dev/coverage.sh
```

It runs the test suite with coverage and writes `coverage/lcov.info`, plus
`coverage/html/` where `genhtml` is installed (`brew install lcov`). Every
library in `lib/` is counted, including the ones no test imports; leaving those
out used to make the total read higher than the truth.

Generated code is left out by the standard `// coverage:ignore-file` comment,
which `flutter test` itself honours, so every way of measuring leaves out the
same files. freezed writes it, `build.yaml` has source_gen add it to every
`.g.dart`, and `l10n.yaml` has gen-l10n add it to the localizations.
`test/tool/generated_code_coverage_test.dart` fails when a generated file comes
out without it.

Nobody has to remember to run it:

- **CI** runs it as the test step and uploads the lcov to
[Codecov](https://app.codecov.io/gh/ExpTechTW/DPIP), which is where the
README's badge and sunburst graph, the per-file view and the pull-request
comment all come from. `codecov.yml` holds its settings; the upload signs
with an OIDC token, so there is no secret to keep.
- **`tool/check.sh`** runs it too, because it is what CI runs.

**In VS Code**, the Dart extension runs it from the Testing view: **Run Tests
with Coverage** puts a bar on every folder and file under Test Coverage, a
percentage beside each file in the Explorer, and the lines that ran in the
editor. That is the extension's own run, so its numbers are close to this
script's rather than equal to them: it leaves out the libraries no test imports,
and it also counts the lines its branch data names.

## Commits

The full specification, with examples, is **[commit.md](commit.md)**. The parts
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[![正式版](https://img.shields.io/github/v/release/exptechtw/dpip?label=%E6%AD%A3%E5%BC%8F%E7%89%88&color=1B8A50)](https://github.com/ExpTechTW/DPIP/releases/latest)
[![測試版](https://img.shields.io/github/v/tag/exptechtw/dpip?sort=date&label=%E6%B8%AC%E8%A9%A6%E7%89%88&color=orange)](https://github.com/ExpTechTW/DPIP/releases)
[![CI](https://img.shields.io/github/actions/workflow/status/ExpTechTW/DPIP/ci.yml?branch=main&label=CI)](https://github.com/ExpTechTW/DPIP/actions/workflows/ci.yml)
[![測試覆蓋率](https://img.shields.io/codecov/c/github/ExpTechTW/DPIP/main?label=%E6%B8%AC%E8%A9%A6%E8%A6%86%E8%93%8B%E7%8E%87)](https://app.codecov.io/gh/ExpTechTW/DPIP)
[![Discord](https://img.shields.io/discord/926545182407688273?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/5dbHqV8ees)

[官網](https://exptech.dev) • [更新日誌](https://github.com/ExpTechTW/DPIP/releases) • [開發文件](AGENTS.md)
Expand Down Expand Up @@ -126,13 +127,20 @@ tool/dev/build.sh ios # iOS(不含簽章)
| 要做什麼 | 指令 |
|---|---|
| 跑測試 | `tool/dev/test.sh` |
| 跑測試並算覆蓋率 | `tool/dev/coverage.sh` |
| 格式化 + 靜態分析 | `tool/dev/analyze.sh` |
| 只格式化 | `tool/dev/format.sh` |
| 重新產生 l10n | `tool/dev/l10n.sh` |
| 重新產生 codegen | `tool/dev/codegen.sh` |
| 砍掉重建 | `tool/dev/clean.sh` |
| 跑完 CI 會跑的每一道關卡 | `tool/check.sh` |

覆蓋率由 CI 上傳到 [Codecov](https://app.codecov.io/gh/ExpTechTW/DPIP)。下圖由內到外是資料夾層級,每個扇形是一個檔案,大小是行數、顏色是覆蓋率:

[![測試覆蓋率旭日圖](https://codecov.io/gh/ExpTechTW/DPIP/branch/main/graphs/sunburst.svg)](https://app.codecov.io/gh/ExpTechTW/DPIP)

產生出來的程式碼不算在內:`flutter test` 會跳過標了 `// coverage:ignore-file` 的檔案,而 `build.yaml` 與 `l10n.yaml` 讓產生器自動加上那一行。

**mise 是必要條件,不是建議。** 沒有 mise 就不能建置這個專案 —— 腳本會直接拒絕執行並告訴你怎麼裝。

**絕對不要自己打 `flutter`、`dart` 或 `mise exec`。** 工具鏈只在 `tool/dev/_lib.sh` 一個地方指定。理由不是整潔:shell 的 PATH 只解析一次,`mise activate` 會把它快取起來,所以升級工具鏈之後舊的 SDK 還留在 PATH 上 —— 而**用錯 SDK 一樣建得起來、跑得起來、測試也會過**,差別要到幾天後變成一個沒人重現得出來的失敗才浮現。
Expand Down
9 changes: 9 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ targets:
# Nested models serialize via their own toJson (so round-trips and
# local caching produce plain JSON, not embedded objects).
explicit_to_json: true
# Generated code is not code anyone tests into shape. `flutter test`
# leaves out every file that says `// coverage:ignore-file`, so CI's
# report and the editor's agree without a list of suffixes kept in step.
# freezed writes the comment into `.freezed.dart` on its own; this puts
# it into every `.g.dart`. test/tool/generated_code_coverage_test.dart
# fails when a generated file comes out without it.
source_gen:combining_builder:
options:
preamble: "// coverage:ignore-file"
25 changes: 25 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# What Codecov does with the lcov CI uploads (.github/workflows/ci.yml).
#
# There is no ignore list here on purpose. Generated code never reaches the
# report: `flutter test` leaves out every file marked `// coverage:ignore-file`,
# which build.yaml and l10n.yaml put into everything the generators write. One
# exclusion, honoured by CI, the editor and anybody running the suite by hand.
coverage:
status:
# The total, against the base commit. A percent of drift is noise — which
# tests happened to load which libraries — rather than a regression, so it
# takes more than that to turn the check red.
project:
default:
target: auto
threshold: 1%

# The lines a pull request adds or changes. It reports and does not block:
# a gate that fails half the pull requests in a codebase still climbing out
# of 55% teaches people to ignore the check rather than to write tests.
patch:
default:
informational: true

comment:
layout: condensed_header, condensed_files, condensed_footer
4 changes: 4 additions & 0 deletions l10n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ output-dir: lib/l10n/gen
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
# Keeps the generated localizations out of test coverage: thousands of getters
# that a string test either reaches or does not would move the total whenever a
# translation is added. `flutter test` honours the comment (see build.yaml).
header: "// coverage:ignore-file"
2 changes: 2 additions & 0 deletions lib/core/geo/town.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/bug_tracker/domain/bug_thread.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/changelog/domain/release_note.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/disaster_map/domain/aed_detail.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/disaster_map/domain/restroom_detail.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/disaster_map/domain/shelter_detail.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/earthquake/domain/earthquake_report.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/earthquake/domain/eew.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/earthquake/domain/rts.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/typhoon/domain/storm_circle.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/typhoon/domain/typhoon_cyclone.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/typhoon/domain/typhoon_potential.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/typhoon/domain/typhoon_track.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/typhoon/domain/typhoon_warning.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/weather/domain/weather_forecast.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/weather/domain/weather_realtime.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/features/weather/domain/weather_station.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/l10n/gen/app_localizations.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// coverage:ignore-file
import 'dart:async';

import 'package:flutter/foundation.dart';
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_en.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_fil.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_id.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_ja.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_ko.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_th.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_vi.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_yue.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/gen/app_localizations_zh.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// coverage:ignore-file

// ignore: unused_import
import 'package:intl/intl.dart' as intl;

Expand Down
Loading
Loading