Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.gitignore export-ignore
.gitattributes export-ignore
pg-travis-test.sh export-ignore
bin/test export-ignore
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,18 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v7
- name: Test the update linter
# First, so a broken instrument reports as a broken instrument rather
# than as a clean (or noisy) SQL diff.
run: make update-lint-test
- name: Check update-script coverage
# Static check that sql/cat_tools--<last released>--<current>.sql.in
# accounts for every object the install scripts differ on. The step
# above already ran this same check on this same pair
# (bin/test/03-real-pairs.t), so this step buys attribution in the CI
# log -- a failure here names the SQL, not the linter -- rather than
# coverage the tests do not already have.
run: make update-lint
- name: Lint SQL
# CRITICAL: call `make lint` directly, not some other path (a script, a
# different target, etc). lint.mk's vendored include is guarded on
Expand Down
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ byte-for-byte copy of `cat_tools.sql.in`, regenerated on every `make`), so
unlike a real release it's ignored too rather than tracked. See
`sql/.gitignore`'s comments and RELEASE.md step 4 for the full detail.

Run `make update-lint` whenever changing the extension's SQL: `bin/update_lint`
statically checks that the update script into the current version accounts
for every object added or removed since the last release — the automated
half of RELEASE.md's "Ongoing development" rule to keep
`sql/cat_tools--<last-released>--stable.sql.in` current. It compares object
identity, not definition, so `bin/structural_diff` remains the authority on
whether a fresh install and an updated one are actually equivalent.
`make update-lint-test` runs the linter's own test suite; both run in the CI
`lint` job.

## CI: PostgreSQL version support

See [`../ai/CLAUDE.md`](../ai/CLAUDE.md) for the general PostgreSQL-version-
Expand Down
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,26 @@ clean_old_version:
# `.vendor/linter/sql/bin/sql-lint sql/cat_tools--0.3.0.sql.in`.
LINT_TARGETS = sql/cat_tools.sql.in test/
include lint.mk

# Static check that the update script into the current version accounts for
# every object the install scripts on either side of it disagree about. Needs
# no database, so it runs in the same cheap CI job as the style linter above;
# see bin/update_lint's header for what it does and does not prove. Like
# LINT_TARGETS, its default scope excludes released pairs, whose files are
# frozen and whose findings could therefore never be fixed.
#
# CRITICAL: this must stay unwired from `lint` in both directions. `lint` only
# exists when lint.mk's vendored include fires, which is guarded on
# $(wildcard .git) -- in a released tarball there is no `lint` target at all
# and `make lint` fails loudly with "No rule to make target". Naming `lint` as
# a prerequisite here (or the reverse) would define it as a real target with no
# recipe, quietly turning that failure into a pass.
.PHONY: update-lint
update-lint:
bin/update_lint

# Unlike update-lint, this needs a checkout: bin/test is export-ignore'd, so it
# is absent from a released tarball.
.PHONY: update-lint-test
update-lint-test:
prove bin/test/
79 changes: 79 additions & 0 deletions bin/test/00-cli.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env perl
#
# Argument handling and exit codes for bin/update_lint.
#
# The exit codes carry the meaning here, so they are what is asserted; message
# wording is deliberately not, apart from the one substring a caller would grep
# for. 2 (usage) versus 0 matters most: an unreadable file that parsed as "zero
# objects" would turn a typo into a green run.

use strict;
use warnings;
use Test::More;
use lib do { require File::Basename; File::Basename::dirname(__FILE__) };
use TestLint;

# -- Help and malformed invocations -------------------------------------------

{
my ($rc, $out, $err) = run('--help');
is($rc, 2, '--help exits 2');
like($err, qr/usage:/, '--help prints usage to stderr');
is($out, '', '--help prints nothing to stdout');
}

usage_exit('unknown option', '--bogus');
usage_exit('two positionals (not three)', 'a', 'b');
usage_exit('four positionals', 'a', 'b', 'c', 'd');
usage_exit('--versions with one version', '--versions', '0.2.0');
usage_exit('--versions combined with positionals',
'--versions', '0.2.0', '0.2.1', 'x', 'y', 'z');
usage_exit('--list-objects combined with positionals',
'--list-objects', '/dev/null', 'x', 'y', 'z');
usage_exit('--list-objects combined with --versions',
'--list-objects', '/dev/null', '--versions', '0.2.0', '0.2.1');
usage_exit('--sql-dir with no value', '--sql-dir');

# -- Unreadable input is a usage error, never a silent empty parse ------------

usage_exit('--list-objects on a missing file', '--list-objects', '/nonexistent/nope.sql');
usage_exit('missing OLD_INSTALL', '/nonexistent/old.sql', '/dev/null', '/dev/null');
usage_exit('missing UPDATE_SCRIPT', '/dev/null', '/dev/null', '/nonexistent/upd.sql');
usage_exit('--versions naming a nonexistent version',
'--versions', '0.0.0', '0.0.1', '--sql-dir', sql_dir());

# A directory opens and reads as the empty string, which is the same shape as
# an unreadable file: nothing parsed, everything clean.
usage_exit('a directory as UPDATE_SCRIPT', '/dev/null', '/dev/null', sql_dir());
usage_exit('--list-objects on a directory', '--list-objects', sql_dir());

# -- Degenerate but legal input ----------------------------------------------

{
my ($rc, $out) = run('--list-objects', '/dev/null');
is($rc, 0, '--list-objects /dev/null exits 0');
is($out, '', '--list-objects /dev/null prints nothing');
}

{
my ($rc, $out) = run('/dev/null', '/dev/null', '/dev/null');
is($rc, 0, 'three empty files compare clean');
like($out, qr/^OK:/m, 'success prints an OK line');
}

# -- Default mode ------------------------------------------------------------

{
my ($rc, $out) = run_in(repo_root());
is($rc, 0, 'default mode is clean on the current source');
like($out, qr/^OK:/m, 'default mode prints an OK line');
}

{
# Default mode reads <ext>.control relative to the working directory, so it
# is a usage error anywhere else rather than a guess at the repo layout.
my ($rc) = run_in(sql_dir());
is($rc, 2, 'default mode outside the extension root exits 2');
}

done_testing();
Loading
Loading