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 .github/scripts/check-compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Check public signatures, dependency metadata and a client compiled against 1.3.1."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import urllib.request
from zipfile import ZipFile
import xml.etree.ElementTree as ET

candidate = Path(sys.argv[1]).resolve()
work = Path('build/compatibility').resolve()
work.mkdir(parents=True, exist_ok=True)
ns = {'m': 'http://maven.apache.org/POM/4.0.0'}
dependencies = ET.parse(candidate.with_suffix('.pom')).findall('.//m:dependency', ns)
assert [[d.find('m:' + k, ns).text for k in ('groupId', 'artifactId', 'version', 'scope')] for d in dependencies] == [
['io.github.lambig', 'Tuplite', '1.0.1', 'compile']]
module = json.loads(candidate.with_suffix('.module').read_text())
for variant in module['variants']:
if variant['name'] in ('apiElements', 'runtimeElements'):
assert variant['attributes']['org.gradle.jvm.version'] == 11
assert variant['dependencies'] == [{'group': 'io.github.lambig', 'module': 'Tuplite', 'version': {'requires': '1.0.1'}}]
for f in variant['files']:
artifact = candidate.parent / f['url']
data = artifact.read_bytes()
assert f['size'] == len(data)
for alg in ('sha512', 'sha256', 'sha1', 'md5'):
if alg in f:
assert hashlib.new(alg, data).hexdigest() == f[alg]

def download(artifact, version):
name = f'{artifact}-{version}.jar'
url = f'https://repo.maven.apache.org/maven2/io/github/lambig/{artifact}/{version}/{name}'
target = work / name
target.write_bytes(urllib.request.urlopen(url, timeout=60).read())
assert hashlib.sha1(target.read_bytes()).hexdigest() == urllib.request.urlopen(url + '.sha1', timeout=60).read().decode().strip()
return target

baseline = download('Patterns', '1.3.1')
old_tuple = download('Tuplite', '1.0.0')
old_fie = download('functional-interface-extension', '1.1.1')
new_tuple = download('Tuplite', '1.0.1')
new_fie = download('functional-interface-extension', '1.1.3')
java_home = Path(os.environ['JAVA_HOME'])
runtime = Path(os.environ.get('TEST_JAVA_HOME', os.environ['JAVA_HOME'])) / 'bin/java'

def classes(jar):
with ZipFile(jar) as archive:
names = sorted(name for name in archive.namelist() if name.endswith('.class'))
for name in names:
assert int.from_bytes(archive.read(name)[6:8], 'big') == 55, name
return [name[:-6].replace('/', '.') for name in names]

types = classes(baseline)
assert types == classes(candidate)
for name in types:
apis = []
for label, jar in [('baseline', baseline), ('candidate', candidate)]:
api = subprocess.check_output([str(java_home / 'bin/javap'), '-public', '-s', '-classpath', str(jar), name], text=True)
(work / (name + '-' + label + '-api.txt')).write_text(api, encoding='utf-8')
apis.append(sorted(line.strip() for line in api.splitlines() if not line.startswith('Compiled from')))
assert apis[0] == apis[1], name + ': public signature changed'

client = work / 'client'
client.mkdir(exist_ok=True)
source = work / 'OldClient.java'
source.write_text('''import io.github.lambig.patterns.*;
import io.github.lambig.tuplite._2.Tuple2;
import java.util.*;
import java.util.function.*;
public class OldClient {
public static void main(String[] args) {
Tuple2<Predicate<Integer>, Function<Integer,String>> rule = Patterns.when(n -> n > 0, Patterns.then("positive"));
Patterns<Integer,String> p = new Patterns<>(List.of(rule));
if (!p.apply(1).equals("positive") || p.getOptionally(0).isPresent()
|| !p.orElse("zero").apply(0).equals("zero")) throw new AssertionError();
List<Integer> seen = new ArrayList<>();
ConsumingPatterns<Integer> c = ConsumingPatterns.of(ConsumingPatterns.when(n -> n > 0, seen::add));
c.accept(1);
c.orElseDo(seen::add).accept(0);
if (!seen.equals(List.of(1,0))) throw new AssertionError();
Throwable cause = new IllegalArgumentException("reason");
NoSuchPatternException e = new NoSuchPatternException(cause);
if (!"reason".equals(e.getMessage()) || e.getCause() != cause) throw new AssertionError();
NoSuchPatternException empty = new NoSuchPatternException();
empty.initCause(cause);
if (empty.getCause() != cause) throw new AssertionError();
System.out.println("Old compiled client: OK");
}
}''', encoding='utf-8')
subprocess.run([str(java_home / 'bin/javac'), '--release', '11', '-cp', os.pathsep.join(map(str, (baseline, old_tuple, old_fie))), '-d', str(client), str(source)], check=True)
for libs in [(baseline, old_tuple, old_fie), (baseline, new_tuple, new_fie), (candidate, new_tuple, new_fie)]:
subprocess.run([str(runtime), '-cp', os.pathsep.join(map(str, (client, *libs))), 'OldClient'], check=True)
print(f'Public signatures preserved: {len(types)} classes; Java 11 bytecode=55')
print(f'Baseline SHA256: {hashlib.sha256(baseline.read_bytes()).hexdigest()}')
print('Old client passed; POM/module export Tuplite 1.0.1; module file hashes match')
25 changes: 25 additions & 0 deletions .github/scripts/check-readme-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Compile and execute both complete programs from the README."""
import os
from pathlib import Path
import re
import subprocess
import sys

work = Path('build/readme-example').resolve()
work.mkdir(parents=True, exist_ok=True)
java_home = Path(os.environ['JAVA_HOME'])
runtime = Path(os.environ.get('TEST_JAVA_HOME', os.environ['JAVA_HOME'])) / 'bin/java'
expected = {
'ClassificationExample': ['large', 'Optional.empty', '[large, small, unknown]'],
'RoutingExample': ['[large:12, other:0]'],
}
blocks = re.findall(r'```java\n(.*?)```', Path('README.md').read_text(encoding='utf-8'), re.S)
assert len(blocks) == len(expected)
for source in blocks:
name = re.search(r'public class (\w+)', source).group(1)
file = work / (name + '.java')
file.write_text(source, encoding='utf-8')
subprocess.run([str(java_home / 'bin/javac'), '--release', '11', '-encoding', 'UTF-8', '-cp', sys.argv[1], '-d', str(work), str(file)], check=True)
result = subprocess.check_output([str(runtime), '-cp', str(work) + os.pathsep + sys.argv[1], name], text=True)
assert result.splitlines() == expected[name], result
print(name + ': ' + result.strip().replace('\n', '; '))
24 changes: 17 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,39 @@ concurrency:

jobs:
verify:
name: Java 11 tests and documentation
name: Java ${{ matrix.java }} contract and consumers
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
java: ['11', '25']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6
with:
distribution: temurin
java-version: '11'
- name: Test and build documentation with the pinned Wrapper
run: bash ./gradlew --no-daemon --console=plain clean test javadocJar sourcesJar
- name: Verify documentation archive contents
run: python3 .github/scripts/check-documentation.py
java-version: ${{ matrix.java }}
- name: Select test runtime
run: echo "TEST_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6
with:
distribution: temurin
java-version: '17'
- name: Verify library, API compatibility and independent consumers
run: sh ./verify-consumers.sh candidate
- name: Preserve test reports and documentation
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: java-11-verification
name: java-${{ matrix.java }}-verification
retention-days: 14
path: |
build/test-results/test/
build/reports/tests/test/
build/libs/*-javadoc.jar
build/libs/*-sources.jar
build/consumer-results/
build/compatibility/*-api.txt
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ hs_err_pid*
.gradle/
build/
.idea/
gradle.properties
gradle.properties
# Maven example output
example/target/
53 changes: 53 additions & 0 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 開発・検証

ビルドにはJDK 17以上、導入例の確認にはMaven 3.8以上、Python 3、POSIXシェルを使います。
Gradleは同梱Wrapperから取得します。ライブラリと導入例はJava 11のAPI・バイトコードを対象にコンパイルします。
`TEST_JAVA_HOME`を指定すると、試験と導入例をそのJavaで実行できます。
CIはビルドJDK 17を用い、実行環境Java 11/25を確認します。

```sh
sh ./verify-consumers.sh candidate
```

このコマンドは単体試験、sources/Javadoc JAR、1.3.1との公開シグネチャ比較、
旧版でコンパイルした利用コード、READMEのプログラム、独立したMaven/Gradle導入例を確認します。
候補版は`build/repository`に生成されます。導入例は一時キャッシュを作り、
Patternsだけを直接の依存として宣言してコンパイル・実行します。
結果は`build/consumer-results/candidate`、試験詳細は`build/reports/tests/test`に残ります。

## 導入例を個別に実行する

```sh
sh ./gradlew publishMavenJavaPublicationToCandidateRepository
sh ./gradlew -p example -PcandidateRepository="$PWD/build/repository" runExample
mvn -f example/pom.xml -DcandidateRepository="file://$PWD/build/repository" \
compile dependency:build-classpath -Dmdep.outputFile=target/runtime-classpath.txt
java -cp "example/target/classes:$(cat example/target/runtime-classpath.txt)" example.PatternExample
```

出力は次のとおりです。

```text
classification=large; fallback=unknown; events=[large:12, other:0]; tuple-api=OK
```

Maven Centralへの公開後は、次のコマンドで公開された1.3.2を新しいキャッシュへ取得し、
同じ導入例とREADMEのプログラムを確認できます。

```sh
sh ./verify-consumers.sh public 1.3.2
```

## 公開用bundle

公開用のASCII形式秘密鍵を`SIGNING_KEY`、そのパスフレーズを`SIGNING_PASSWORD`に設定して実行します。
認証情報は環境変数で渡します。

```sh
sh ./gradlew clean check -PreleaseBundle centralBundle
```

`build/central/Patterns-1.3.2-central.zip`には本体、sources、Javadoc、POM、
Gradle Module Metadataと、それぞれの署名・チェックサムが入ります。
公開者は対象コミット、検証結果とbundle内容を確認したうえでCentral Portalへ登録します。
公開後は上記の`public`による取得・導入確認を行います。
Loading