Files
Ustagammon/tests/test_reference.sh

168 lines
7.5 KiB
Bash

#!/usr/bin/env bash
# Vérifie la cohérence des positions de référence (tests/reference/) :
# JSON valide, au moins 30 positions, chaque catégorie présente, un résultat
# par position aux niveaux 0 et 2 plis avec tous les champs du format
# (voir tests/reference/README.md). Ne lance pas gnubg : jq tourne dans
# l'image de la chaîne C (ADR 0004), dans laquelle le script se relance.
set -euo pipefail
cd "$(dirname "$0")/.."
if [ -z "${USTAGAMMON_IN_DOCKER:-}" ]; then
image="${BUILD_C_IMAGE:?lancer via make test}"
docker image inspect "$image" >/dev/null 2>&1 \
|| { echo "Chaîne C absente : lancer d'abord make setup"; exit 1; }
exec docker run --rm --user "$(id -u):$(id -g)" -e USTAGAMMON_IN_DOCKER=1 \
-v "$PWD:/src:ro" -w /src "$image" bash tests/test_reference.sh
fi
cd tests/reference
erreurs=0
signaler() {
echo "$1"
erreurs=$((erreurs + 1))
}
jq empty positions.json || { echo "positions.json : JSON invalide"; exit 1; }
nombre="$(jq '.positions | length' positions.json)"
[ "$nombre" -ge 30 ] || signaler "positions.json : $nombre positions, 30 au minimum"
doublons="$(jq -r '[.positions[].id] | group_by(.) | map(select(length > 1)[0]) | join(" ")' positions.json)"
[ -z "$doublons" ] || signaler "positions.json : identifiants en double : $doublons"
for categorie in opening middlegame prime blitz race bearoff \
cube_no_double cube_double_take cube_double_pass match crawford jacoby; do
jq -e --arg c "$categorie" 'any(.positions[]; .categories | index($c))' positions.json >/dev/null \
|| signaler "positions.json : aucune position de catégorie « $categorie »"
done
# Champs obligatoires de chaque position d'entrée.
invalides="$(jq -r '.positions[]
| select((.id | type) != "string"
or (.type | IN("move", "cube") | not)
or (.position_id | test("^[A-Za-z0-9+/]{14}$") | not)
or (.match_id | test("^[A-Za-z0-9+/]{12}$") | not)
or (.categories | type) != "array"
or (.description | type) != "string"
or (if .type == "move" then (.dice | type) != "array" else .dice != null end))
| "positions.json : position \(.id) incomplète ou invalide"' positions.json)"
[ -z "$invalides" ] || signaler "$invalides"
# Filtre jq : liste les défauts d'un fichier de résultat (vide si conforme).
read -r -d '' controle <<'JQ' || true
def nombre: type == "number";
def probas: (keys == ["lose_backgammon", "lose_gammon", "win", "win_backgammon", "win_gammon"])
and all(.[]; nombre and . >= 0 and . <= 1);
def defauts($pos):
(if .id != $pos.id or .type != $pos.type or .position_id != $pos.position_id
or .match_id != $pos.match_id or .categories != $pos.categories
then "en-tête différent de positions.json" else empty end),
(if (.gnubg_commit | test("^[0-9a-f]{40}$")) and .met == "Kazaross-XG2" then empty
else "gnubg_commit ou met invalide" end),
(.context | if (.dice == $pos.dice) and (.match_length | nombre)
and (.score_player | nombre) and (.score_opponent | nombre)
and (.cube_value | nombre) and (.cube_owner | IN("centered", "player", "opponent"))
and ([.crawford, .jacoby, .beavers] | all(type == "boolean"))
then empty else "context incomplet" end),
(if (.plies | keys) == ["0", "2"] then empty else "niveaux attendus : 0 et 2" end),
(.plies | to_entries[] | .key as $n | .value |
(if (.evaluation.probabilities | probas) and (.evaluation.equity | nombre)
then empty else "niveau \($n) : evaluation incomplète" end),
(if $pos.type == "move" then
(.moves | if (.legal_move_count | nombre)
and (.ranking | length) == ([.legal_move_count, 10] | min)
and all(.ranking[]; (.rank | nombre) and (.move | type) == "string"
and (.ply | nombre) and (.equity | nombre) and (.equity_diff | nombre)
and (.probabilities | probas))
then empty else "niveau \($n) : classement des coups incomplet" end)
else
(.cube | if ([.no_double, .double_take, .double_pass, .optimal] | all(nombre))
and (.decision | type) == "string" and (.decision_text | type) == "string"
then empty else "niveau \($n) : décision de videau incomplète" end)
end));
defauts($pos)
JQ
# Décision de videau attendue à 2 plis selon la catégorie.
read -r -d '' decision <<'JQ' || true
.categories as $c | .plies["2"].cube.decision as $d
| if ($c | index("cube_no_double")) and ($d | test("^NO_?(RE)?DOUBLE_") | not) then "pas de double attendu, gnubg dit \($d)"
elif ($c | index("cube_double_take")) and ($d | IN("DOUBLE_TAKE", "REDOUBLE_TAKE") | not) then "double/prend attendu, gnubg dit \($d)"
elif ($c | index("cube_double_pass")) and ($d | IN("DOUBLE_PASS", "REDOUBLE_PASS") | not) then "double/passe attendu, gnubg dit \($d)"
else empty end
JQ
mapfile -t ids < <(jq -r '.positions[].id' positions.json)
for id in "${ids[@]}"; do
f="results/$id.json"
if [ ! -f "$f" ]; then
signaler "$f absent : lancer make reference"
continue
fi
if ! jq empty "$f" 2>/dev/null; then
signaler "$f : JSON invalide"
continue
fi
pos="$(jq --arg id "$id" '.positions[] | select(.id == $id)' positions.json)"
while read -r defaut; do
[ -n "$defaut" ] && signaler "$f : $defaut"
done < <(jq -r --argjson pos "$pos" "$controle" "$f"; jq -r "$decision" "$f")
done
for f in results/*.json; do
[ -e "$f" ] || continue
id="$(basename "$f" .json)"
printf '%s\n' "${ids[@]}" | grep -qxF "$id" || signaler "$f : position absente de positions.json"
done
# Analyses (README.md, section « Analyses ») : un résultat par cas, et les
# champs du format.
jq empty analyses.json || { echo "analyses.json : JSON invalide"; exit 1; }
read -r -d '' controle_analyse <<'JQ' || true
def nombre: type == "number";
def skill: IN("none", "doubtful", "bad", "very_bad");
if .type == "move" then
(if (.plies | keys) == ["0", "2"] and (.luck.value | nombre)
and (.luck.rating | IN("very_unlucky", "unlucky", "none", "lucky", "very_lucky"))
and all(.plies[]; (.move_count | nombre) and (.rank | nombre) and (.played_move | type) == "string"
and (.played_position_id | test("^[A-Za-z0-9+/]{14}$")) and (.played_equity | nombre)
and (.best_move | type) == "string" and (.best_equity | nombre)
and (.equity_loss | nombre) and .equity_loss <= 0 and (.skill | skill)
and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max))
then empty else "analyse de coup incomplète" end)
elif .type == "cube" then
(if (.plies | keys) == ["0", "2"] and (.action | IN("no_double", "double", "take", "pass"))
and all(.plies[]; (.analysed | type) == "boolean" and (.equity_loss | nombre)
and .equity_loss <= 0 and (.skill | skill)
and (if .analysed then (.cube.decision | type) == "string" else .cube == null end))
then empty else "analyse de videau incomplète" end)
elif .type == "level" then
(if (.levels | length) > 0 and all(.levels[]; (.plies | nombre) and (.noise | nombre)
and ((.move | type) == "string" or (.decision | type) == "string"))
then empty else "niveaux incomplets" end)
else "type inconnu" end
JQ
mapfile -t analyses < <(jq -r '(.moves + .cubes + .levels)[].id' analyses.json)
doublons="$(printf '%s\n' "${analyses[@]}" "${ids[@]}" | sort | uniq -d | paste -sd' ' -)"
[ -z "$doublons" ] || signaler "analyses.json : identifiants en double : $doublons"
for id in "${analyses[@]}"; do
f="analysis/$id.json"
if [ ! -f "$f" ]; then
signaler "$f absent : lancer make reference"
continue
fi
while read -r defaut; do
[ -n "$defaut" ] && signaler "$f : $defaut"
done < <(jq -r "$controle_analyse" "$f")
done
for f in analysis/*.json; do
[ -e "$f" ] || continue
printf '%s\n' "${analyses[@]}" | grep -qxF "$(basename "$f" .json)" \
|| signaler "$f : cas absent d'analyses.json"
done
[ "$erreurs" -eq 0 ]