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
97 changes: 97 additions & 0 deletions deploy/parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,103 @@ func TestNeitherInstallerPutsASecretOnTheBinaryCommandLine(t *testing.T) {
}
}

// devScriptPath and devPowerShellScriptPath are dev.sh and dev.ps1 — the "check and
// guide" pair at the repository root, next to make.ps1. deploy/ tests run one level
// below the root, hence the "..".
var (
devScriptPath = filepath.Join("..", "dev.sh")
devPowerShellScriptPath = filepath.Join("..", "dev.ps1")
)

// devGroupReasonException is the one legitimate asymmetry between dev.sh and dev.ps1:
// dev.sh checks for docker-group membership and dev.ps1 does not, because only Linux
// has a docker group to be missing from. Same rule as installerParityExceptions above —
// a silent écart is a carpet, a written one is an arbitrage — so it is held by the same
// helper rather than a new one.
var devGroupReasonException = parityException{
option: "le contrôle du groupe docker",
carriedBy: devScriptPath,
missingFrom: devPowerShellScriptPath,
why: "dev.sh signale quand l'utilisateur Linux n'est pas dans le groupe docker (« " +
"permission denied … /var/run/docker.sock ») parce que Docker Desktop pour Windows " +
"n'a pas d'équivalent : son démon est exposé par un named pipe que gère son propre " +
"service, pas par les permissions d'un groupe Unix — il n'y a donc rien à détecter " +
"côté PowerShell.",
proof: "groupe docker",
}

// devCheckMarkers are the three checks dev.sh and dev.ps1 must both perform, in the
// order they must perform them.
//
// "devcontainer --version" and not the bare "devcontainer": the second is a substring of
// the third ("devcontainer up"), so it is true by construction as soon as the third is —
// it cannot, on its own, catch the second check disappearing. "--version" is what both
// scripts actually run to tell "the CLI is available" from "a command of this name is
// merely on the PATH" (see the comment next to each call site) — using it here means this
// bench would have caught that check going back to a bare presence test.
var devCheckMarkers = []string{"docker info", "devcontainer --version", "devcontainer up"}

// TestDevScriptsCheckTheSameThings is the parity guard for dev.sh and dev.ps1 — the "one
// command that checks and guides" pair, and neither one an installer.
//
// Both scripts are deliberately option-free (see both headers), so there is no table of
// flags to compare the way installerParity does above. Parity here means the same THREE
// checks (devCheckMarkers), in the same order, read out of both files as text, the way
// installerParity's neighbours already do: actually RUNNING either script from this bench
// would need Docker and Node on whatever machine runs `go test ./deploy/`, which is
// exactly what these scripts exist to check for instead.
func TestDevScriptsCheckTheSameThings(t *testing.T) {
sh := codeOnly(readFile(t, devScriptPath))
ps1 := codeOnly(readFile(t, devPowerShellScriptPath))

// Present on both sides — and the message says what it can actually see rather than
// assuming the side it is not looking at: a check missing from BOTH scripts is a
// different failure from one that fell off a single side, and conflating them would
// have this bench contradict itself on exactly the case where both regressed together.
for _, marker := range devCheckMarkers {
inSh := strings.Contains(sh, marker)
inPs1 := strings.Contains(ps1, marker)
switch {
case inSh && inPs1:
// rien à dire
case inSh && !inPs1:
t.Errorf("dev.ps1 ne contient pas %q, alors que dev.sh le porte : un des trois "+
"contrôles a disparu d'un seul côté", marker)
case !inSh && inPs1:
t.Errorf("dev.sh ne contient pas %q, alors que dev.ps1 le porte : un des trois "+
"contrôles a disparu d'un seul côté", marker)
default:
t.Errorf("ni dev.sh ni dev.ps1 ne contiennent %q : un des trois contrôles a "+
"disparu des deux côtés", marker)
}
}

// Dans le même ordre — sur les marqueurs effectivement trouvés seulement : un marqueur
// absent est déjà signalé ci-dessus, et le juger aussi sur l'ordre ne ferait que
// répéter la même panne sous un second message.
for _, script := range []struct{ name, text string }{{"dev.sh", sh}, {"dev.ps1", ps1}} {
last := -1
for _, marker := range devCheckMarkers {
at := strings.Index(script.text, marker)
if at < 0 {
continue
}
if at < last {
t.Errorf("%s : %q apparaît avant un contrôle qui devrait le précéder — les "+
"trois contrôles doivent se succéder dans le même ordre des deux côtés",
script.name, marker)
}
last = at
}
}

if !strings.Contains(strings.ToLower(sh), devGroupReasonException.proof) {
t.Errorf("dev.sh ne nomme plus %s : la raison écrite dans dev.ps1 excuse un "+
"contrôle qui n'existe plus", devGroupReasonException.proof)
}
checkTheReasonIsWritten(t, devGroupReasonException)
}

// --- Les lecteurs ---------------------------------------------------------------------

// dashedSpelling renders a PowerShell parameter the way a sh script spells it.
Expand Down
17 changes: 10 additions & 7 deletions deploy/powershell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,14 +618,17 @@ func measurementsOf(output string) map[string]string {
// station half-installed, and the typo is found by whoever runs it as administrator on a
// Saturday morning.
//
// It uses the PowerShell parser itself rather than a heuristic, and it checks the four
// scripts plus the shared file — under EVERY PowerShell installed, because the encoding
// defect above is invisible to PowerShell 7 and fatal to 5.1.
// It uses the PowerShell parser itself rather than a heuristic, and it now checks EVERY
// PowerShell script of the repository — via powerShellScripts, not a glob of its own —
// under EVERY PowerShell installed, because the encoding defect above is invisible to
// PowerShell 7 and fatal to 5.1.
//
// The four installers and common.ps1 were always covered this way. make.ps1 and dev.ps1
// were not: a glob scoped to windows/*.ps1 never saw either, so the script every Windows
// contributor runs first — make.ps1 — was parsed by no interpreter at all, a hole dev.ps1
// would otherwise have inherited on day one.
func TestEveryPowerShellScriptParses(t *testing.T) {
scripts, err := filepath.Glob(filepath.Join("windows", "*.ps1"))
if err != nil || len(scripts) == 0 {
t.Fatalf("aucun script PowerShell trouvé : %v", err)
}
scripts := powerShellScripts(t)

body := `$ErrorActionPreference = 'Stop'
$failed = 0
Expand Down
13 changes: 10 additions & 3 deletions deploy/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
// TestNoShellScriptExitsOnATestThatIsSimplyFalse guards a trap `sh -n` cannot see, and
// that a Saturday-morning installation would find instead.
//
// dev.sh joins the list too, for the same reason it joined the `sh -n` bench next to this
// one: it poses `set -e` and is exactly the kind of script a first-run failure must speak
// from, not exit silently on a test that was simply false.
//
// Under `set -e`, a standalone `[ … ] && commande` whose TEST is false returns a non-zero
// status, and the shell exits. It reads like « fais ceci si », it behaves like « arrête-toi
// si ce n'est pas le cas ». It was really in install.sh: an optional file that is not
Expand All @@ -31,6 +35,7 @@ func TestNoShellScriptExitsOnATestThatIsSimplyFalse(t *testing.T) {
if err != nil || len(scripts) == 0 {
t.Fatalf("aucun script shell trouvé : %v", err)
}
scripts = append(scripts, filepath.Join("..", "dev.sh"))
andList := regexp.MustCompile(`^\s*(\[|command\s|test\s).*&&`)

for _, script := range scripts {
Expand Down Expand Up @@ -92,9 +97,10 @@ func TestNoLinuxArtifactCarriesAWindowsLineEnding(t *testing.T) {

// TestTheShellScriptsAreValidAccordingToTheShell runs `sh -n` when a shell is available.
//
// .devcontainer/post-create.sh joins the list: it is not under linux/, but a syntax error in
// it is otherwise discovered only after an eight-minute container build, while `sh -n` costs
// nothing and runs on the same Linux CI that already builds this list.
// The list reaches outside linux/ for two files now, and for the same reason each time: a
// syntax error in either is otherwise discovered late — after an eight-minute container
// build for .devcontainer/post-create.sh, on a contributor's first run for dev.sh — while
// `sh -n` costs nothing and runs on the same Linux CI that already builds this list.
func TestTheShellScriptsAreValidAccordingToTheShell(t *testing.T) {
shell, err := exec.LookPath("sh")
if err != nil {
Expand All @@ -105,6 +111,7 @@ func TestTheShellScriptsAreValidAccordingToTheShell(t *testing.T) {
t.Fatalf("aucun script shell trouvé : %v", err)
}
scripts = append(scripts, filepath.Join("..", ".devcontainer", "post-create.sh"))
scripts = append(scripts, filepath.Join("..", "dev.sh"))
for _, script := range scripts {
output, err := exec.Command(shell, "-n", script).CombinedOutput()
if err != nil {
Expand Down
132 changes: 132 additions & 0 deletions dev.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<#
.SYNOPSIS
Vérifie qu'un poste peut lancer le conteneur de développement, et dit quoi faire sinon.

.DESCRIPTION
Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a
que Docker : le chemin conteneur du guide de démarrage (handbook/getting-started.md)
rejoue déjà, à la main, tout ce que la CI vérifie sauf les scripts d'installation sous
PowerShell 5.1, qu'aucun conteneur Linux ne peut exécuter. Mais ses pannes de premier
lancement ne se racontent pas d'elles-mêmes -- Docker installé mais pas démarré, ou la
CLI devcontainer trouvable sans être exécutable. Ce script fait trois contrôles DANS
CET ORDRE et s'arrête sur le premier qui échoue, en disant quoi faire.

CE SCRIPT N'INSTALLE RIEN -- ni Docker, ni Node. Les installer demande des droits
administrateur, un installeur graphique, et pour Docker Desktop un redémarrage ; un
script du dépôt qui tenterait ça sur la machine de quelqu'un d'autre échouerait en
silence, ce qu'une commande « qui vérifie et guide » existe justement pour éviter. Il
contrôle, il nomme, il lance.

Pas d'options, et c'est délibéré : ça garde la surface de parité avec dev.sh (voir
deploy/parity_test.go) réduite à trois contrôles, plutôt qu'à une table de réglages à
tenir en phase des deux côtés.

CE SCRIPT DOIT TOURNER SOUS WINDOWS POWERSHELL 5.1, comme make.ps1 : c'est le seul
PowerShell garanti sur un poste Windows neuf. Aucune syntaxe propre à 7 (`??`, `?.`,
l'opérateur ternaire) n'y est employée.

.EXAMPLE
.\dev.ps1
#>

$ErrorActionPreference = 'Stop'

# Test-CommandRuns renvoie si l'appel réussit (code de sortie 0), sans jamais laisser une
# ÉCRITURE SUR STDERR transformer un succès en échec.
#
# Sous Windows PowerShell 5.1 -- pas sous pwsh 7 -- $ErrorActionPreference = 'Stop' rend
# TERMINANTE toute écriture sur le flux d'erreur d'une commande NATIVE dont la sortie est
# redirigée (« *> $null » ci-dessous), même quand cette commande réussit et n'écrit là
# qu'un avertissement. Mesuré sur ce poste, sous les deux PowerShell : un
# « cmd /c "echo err 1>&2 && exit 0" » réussi devient une exception attrapée par le
# catch sous 5.1, jamais sous 7. « docker info » sur un moteur WSL2 émet un « WARNING: »
# sur stderr, et « devcontainer --version » hérite de tout avertissement expérimental que
# Node écrit là -- les deux auraient donc été déclarés en panne sous 5.1 alors qu'ils
# répondent.
#
# La préférence est desserrée le temps de CET appel seulement, et remise aussitôt après :
# une commande ABSENTE lève toujours une exception, quelle que soit la préférence, donc la
# branche « non installé » des deux contrôles continue de fonctionner sans elle.
#
# NE PAS « SIMPLIFIER » CE DÉTOUR : il tient la seule différence mesurée entre 5.1 et 7 sur
# ce script, et aucun banc du dépôt ne peut l'exécuter pour le revérifier -- le parseur de
# deploy/powershell_test.go ANALYSE ces scripts, il ne les fait pas tourner.
function Test-CommandRuns([scriptblock]$Command) {
$previous = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $Command *> $null
return ($LASTEXITCODE -eq 0)
}
catch {
return $false
}
finally {
$ErrorActionPreference = $previous
}
}

Write-Host '1. Docker'

# « docker info » et non « Get-Command docker » : un Docker installé mais pas démarré est
# le cas ordinaire, et seul « docker info » distingue les deux. Get-Command ne sert
# ci-dessous qu'à CHOISIR le bon message une fois ce contrôle-là en échec.
$dockerReady = Test-CommandRuns { docker info }

if (-not $dockerReady) {
if (Get-Command docker -ErrorAction SilentlyContinue) {
Write-Host " La commande docker existe mais ne répond pas. Cause la plus probable :"
Write-Host " - Docker Desktop n'est pas démarré : lancez-le depuis le menu Démarrer."
Write-Host " - (WSL2) la distribution Linux qui héberge le moteur n'est pas lancée."
Write-Host ''
Write-Host " Il n'y a pas de groupe docker sous Windows : Docker Desktop expose son"
Write-Host " démon par un named pipe que gère son propre service, pas par les"
Write-Host " permissions d'un groupe Unix comme le fait dev.sh sous Linux."
}
else {
Write-Host " Docker n'est pas installé. Sur cette machine :"
Write-Host ' Installez Docker Desktop :'
Write-Host ' https://docs.docker.com/desktop/setup/install/windows-install/'
}
exit 1
}
Write-Host ' Docker répond.'

Write-Host ''
Write-Host '2. La CLI devcontainer'

# « devcontainer --version » et non « Get-Command devcontainer », pour la même raison que
# le contrôle 1 : une commande présente dans le PATH n'est pas forcément exécutable telle
# quelle -- voir le commentaire équivalent de dev.sh, où c'est mesuré sous WSL avec un
# devcontainer installé côté Windows mais injoignable depuis Linux.
$devcontainerReady = Test-CommandRuns { devcontainer --version }

if (-not $devcontainerReady) {
Write-Host " La commande devcontainer est introuvable ou ne fonctionne pas. Installez-la :"
Write-Host ' npm i -g @devcontainers/cli'
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
Write-Host ''
Write-Host " npm est absent : installez Node d'abord."
Write-Host ' winget install OpenJS.NodeJS.LTS'
Write-Host ' (ou : https://nodejs.org/en/download)'
}
Write-Host ''
Write-Host " Un éditeur qui sait ouvrir un devcontainer (VS Code, Cursor, Windsurf) n'a"
Write-Host " besoin d'aucun Node pour ça : son extension parle au démon Docker directement."
exit 1
}
Write-Host ' devcontainer est disponible.'

Write-Host ''
Write-Host '3. Tout est présent -- lancement du conteneur de développement'

devcontainer up --workspace-folder .
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

Write-Host ''
Write-Host 'Poste prêt. Ce que vous pouvez rejouer depuis ce conteneur :'
Write-Host ' devcontainer exec --workspace-folder . make test'
Write-Host ' devcontainer exec --workspace-folder . make front-check'
Write-Host ' devcontainer exec --workspace-folder . mkdocs build --strict'
Loading