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
4 changes: 3 additions & 1 deletion .github/workflows/e2e-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ jobs:
env:
JAVA_VERSION: ${{ matrix.version }}
JAVA_PATH: ${{ steps.setup-java.outputs.path }}
run: bash __tests__/verify-java.sh "$JAVA_VERSION" "$JAVA_PATH"
SETUP_JAVA_VERSION: ${{ steps.setup-java.outputs.version }}
REQUIRE_CONCRETE_VERSION: ${{ (matrix.distribution == 'oracle' || matrix.distribution == 'graalvm') && !contains(matrix.version, '.') && !contains(matrix.version, '-ea') }}
run: bash __tests__/verify-java.sh "$JAVA_VERSION" "$JAVA_PATH" "$SETUP_JAVA_VERSION" "$REQUIRE_CONCRETE_VERSION"
shell: bash

setup-java-checksum-verification:
Expand Down
234 changes: 232 additions & 2 deletions __tests__/distributors/base-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,47 @@ class EmptyJavaBase extends JavaBase {
}
}

class FloatingJavaBase extends JavaBase {
static actualVersion = '21.0.8+9';
static checksum: string | undefined = 'artifact-one';
static fingerprint: string | undefined = undefined;

constructor(installerOptions: JavaInstallerOptions) {
super('Floating', installerOptions);
}

protected async downloadTool(): Promise<JavaInstallerResults> {
return {
version: FloatingJavaBase.actualVersion,
path: path.join(
'toolcache',
this.toolcacheFolderName,
FloatingJavaBase.actualVersion.replace('+', '-'),
this.architecture
)
};
}

protected async findPackageForDownload(): Promise<JavaDownloadRelease> {
return {
version: '21',
url: 'https://example.com/java/21/latest/jdk-21.tar.gz',
checksum: FloatingJavaBase.checksum
? {
algorithm: 'sha256',
value: FloatingJavaBase.checksum
}
: undefined,
floating: true,
fingerprint: FloatingJavaBase.fingerprint
};
}

protected requiresRemoteResolution(): boolean {
return true;
}
}

describe('findInToolcache', () => {
const actualJavaVersion = '11.0.8';
const javaPath = path.join('Java_Empty_jdk', actualJavaVersion, 'x64');
Expand Down Expand Up @@ -397,6 +438,7 @@ describe('setupJava', () => {
spyCoreError.mockImplementation(() => undefined);

jest.spyOn(os, 'arch').mockReturnValue('x86' as ReturnType<typeof os.arch>);
FloatingJavaBase.fingerprint = undefined;
});

afterEach(() => {
Expand Down Expand Up @@ -476,6 +518,179 @@ describe('setupJava', () => {
);
});

it('uses the concrete versions of two different floating artifacts under the same major', async () => {
spyTcFindAllVersions.mockReturnValue(['21.0.8-9']);
spyGetToolcachePath.mockImplementation(
(_toolname: string, version: string, architecture: string) =>
path.join('toolcache', 'Java_Floating_jdk', version, architecture)
);
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue(
undefined
);
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);

FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = 'artifact-one';
const first = new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
});
await expect(first.setupJava()).resolves.toEqual({
version: '21.0.8+9',
path: path.join('toolcache', 'Java_Floating_jdk', '21.0.8-9', 'x64')
});

FloatingJavaBase.actualVersion = '21.0.9+7';
FloatingJavaBase.checksum = 'artifact-two';
const second = new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
});
await expect(second.setupJava()).resolves.toEqual({
version: '21.0.9+7',
path: path.join('toolcache', 'Java_Floating_jdk', '21.0.9-7', 'x64')
});

expect(spyCoreSetOutput).toHaveBeenNthCalledWith(3, 'version', '21.0.8+9');
expect(spyCoreSetOutput).toHaveBeenNthCalledWith(6, 'version', '21.0.9+7');
expect(jdkCache.registerJdk).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
version: '21.0.8+9',
source: 'sha256:artifact-one'
})
);
expect(jdkCache.registerJdk).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
version: '21.0.9+7',
source: 'sha256:artifact-two'
})
);
expect(jdkResolutionCache.registerJdkResolution).toHaveBeenNthCalledWith(
2,
expect.objectContaining({source: 'sha256:artifact-two'}),
expect.objectContaining({version: '21.0.9+7', floating: true})
);
});

it('does not trust a matching tool-cache version for a floating artifact', async () => {
spyTcFindAllVersions.mockReturnValue(['21.0.8-9']);
spyGetToolcachePath.mockReturnValue(
path.join('toolcache', 'Java_Floating_jdk', '21.0.8-9', 'x64')
);
(jdkResolutionCache.restoreJdkResolution as jest.Mock).mockResolvedValue({
release: {
version: '21.0.8+9',
url: 'https://example.com/java/21/latest/jdk-21.tar.gz',
checksum: {algorithm: 'sha256', value: 'artifact-republished'},
floating: true
},
fresh: true
});
(jdkCache.restoreJdk as jest.Mock).mockResolvedValue(false);
FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = 'artifact-republished';
const distribution = new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
});
const downloadTool = jest.spyOn(distribution as any, 'downloadTool');

await distribution.setupJava();

expect(jdkCache.restoreJdk).toHaveBeenCalled();
expect(downloadTool).toHaveBeenCalled();
});

it('does not cache a floating artifact with no way to identify its bytes', async () => {
spyTcFindAllVersions.mockReturnValue(['21.0.8-9']);
spyGetToolcachePath.mockReturnValue(
path.join('toolcache', 'Java_Floating_jdk', '21.0.8-9', 'x64')
);
FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = undefined;
FloatingJavaBase.fingerprint = undefined;
const distribution = new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
});

await distribution.setupJava();

expect(jdkResolutionCache.restoreJdkResolution).not.toHaveBeenCalled();
expect(jdkResolutionCache.registerJdkResolution).not.toHaveBeenCalled();
expect(jdkCache.restoreJdk).not.toHaveBeenCalled();
expect(jdkCache.registerJdk).not.toHaveBeenCalled();
});

it('caches a checksum-less floating artifact identified by its response fingerprint', async () => {
spyTcFindAllVersions.mockReturnValue([]);
spyGetToolcachePath.mockReturnValue(
path.join('toolcache', 'Java_Floating_jdk', '21.0.8-9', 'x64')
);
FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = undefined;
FloatingJavaBase.fingerprint = 'etag:"artifact-one"';
const distribution = new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
});

await distribution.setupJava();

// The fingerprint changes when the vendor republishes, so it is a safe
// identity even though no checksum is available.
expect(jdkResolutionCache.registerJdkResolution).toHaveBeenCalledWith(
expect.objectContaining({source: 'etag:"artifact-one"'}),
expect.objectContaining({version: '21.0.8+9'})
);
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
expect.objectContaining({source: 'etag:"artifact-one"'})
);
});

it('separates the cache identities of two builds served by the same floating URL', async () => {
const sources: string[] = [];
(jdkCache.registerJdk as jest.Mock).mockImplementation((entry: any) => {
sources.push(entry.source);
});
spyTcFindAllVersions.mockReturnValue([]);
spyGetToolcachePath.mockReturnValue(
path.join('toolcache', 'Java_Floating_jdk', '21.0.8-9', 'x64')
);
FloatingJavaBase.actualVersion = '21.0.8+9';
FloatingJavaBase.checksum = undefined;

for (const fingerprint of ['etag:"before"', 'etag:"after"']) {
FloatingJavaBase.fingerprint = fingerprint;
await new FloatingJavaBase({
version: '21',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
cacheJdk: true
}).setupJava();
}

expect(sources).toEqual(['etag:"before"', 'etag:"after"']);
});

it('should download java when force-download is enabled, even if the version is cached', async () => {
mockJavaBase = new EmptyJavaBase({
version: actualJavaVersion,
Expand Down Expand Up @@ -1074,7 +1289,7 @@ describe('setupJava', () => {
);
});

it('does not record a floating release', async () => {
it('records the concrete version for a checksum-bound floating release', async () => {
mockJavaBase = new EmptyJavaBase(options);
jest
.spyOn(mockJavaBase as any, 'findPackageForDownload')
Expand All @@ -1087,7 +1302,22 @@ describe('setupJava', () => {

await mockJavaBase.setupJava();

expect(jdkResolutionCache.registerJdkResolution).not.toHaveBeenCalled();
expect(jdkResolutionCache.registerJdkResolution).toHaveBeenCalledWith(
{
distribution: 'Empty',
packageType: 'jdk',
architecture: 'x86',
versionSpec: '11.0.9',
stable: true,
source: 'sha256:abc'
},
{
version: '11.0.9',
url: 'https://example.com/java/11/latest/jdk-11.tar.gz',
checksum: {algorithm: 'sha256', value: 'abc'},
floating: true
}
);
});

it.each([
Expand Down
52 changes: 52 additions & 0 deletions __tests__/distributors/graalvm-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
...realUtil,
extractJdkFile: jest.fn(),
getDownloadArchiveExtension: jest.fn(),
getJavaVersionFromReleaseFile: jest.fn(),
renameWinArchive: jest.fn(),
getGitHubHttpHeaders: jest.fn().mockReturnValue({Accept: 'application/json'})
}));
Expand Down Expand Up @@ -363,6 +364,30 @@ describe('GraalVMDistribution', () => {
path: '/cached/java/path'
});
});

it('caches Oracle GraalVM floating artifacts under their installed version', async () => {
(util.getJavaVersionFromReleaseFile as jest.Mock<any>).mockReturnValue(
'21.0.9+7'
);
const floatingRelease = {
version: '21',
url: 'https://example.com/graalvm/latest/graalvm-jdk-21.tar.gz',
floating: true
};

const result = await (distribution as any).downloadTool(floatingRelease);

expect(tc.cacheDir).toHaveBeenCalledWith(
path.join('/tmp/extracted', 'graalvm-jdk-17.0.5'),
'Java_GraalVM_jdk',
'21.0.9+7',
'x64'
);
expect(result).toEqual({
version: '21.0.9+7',
path: '/cached/java/path'
});
});
});

describe('findPackageForDownload', () => {
Expand Down Expand Up @@ -451,6 +476,33 @@ describe('GraalVMDistribution', () => {
});
});

it.each([
['21', 'etag:"graalvm-latest"'],
['17.0.5', undefined]
])(
'fingerprints only the floating artifact for version %s',
async (input, expected) => {
mockHttpClient.head.mockResolvedValue({
message: {statusCode: 200, headers: {etag: '"graalvm-latest"'}}
} as any);

const result = await (distribution as any).findPackageForDownload(
input
);

// Without a fingerprint the constant `/latest/` URL would key a cache
// entry that never invalidates when Oracle republishes the artifact.
expect(result.fingerprint).toBe(expected);
}
);

it('always resolves Oracle GraalVM major-only requests remotely', () => {
expect((distribution as any).requiresRemoteResolution()).toBe(true);
expect((communityDistribution as any).requiresRemoteResolution()).toBe(
false
);
});

it('should throw error for unsupported architecture', async () => {
distribution = new GraalVMDistribution({
...defaultOptions,
Expand Down
32 changes: 32 additions & 0 deletions __tests__/distributors/oracle-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,27 @@ describe('findPackageForDownload', () => {
expect(result.floating).toBe(url.includes('/latest/'));
});

it.each([
['21', 'etag:"oracle-latest"'],
['21.0.1', undefined]
])(
'fingerprints only the floating artifact for version %s',
async (input, expected) => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'head');
spyHttpClient.mockResolvedValue({
message: {statusCode: 200, headers: {etag: '"oracle-latest"'}}
});

const result = await distribution['findPackageForDownload'](input);

jest.restoreAllMocks();

// Without a fingerprint the constant `/latest/` URL would key a cache
// entry that never invalidates when Oracle republishes the artifact.
expect(result.fingerprint).toBe(expected);
}
);

it('fetches the authoritative sha256 checksum for the resolved archive', async () => {
spyHttpClient = jest.spyOn(HttpClient.prototype, 'head');
spyHttpClient.mockResolvedValue({message: {statusCode: 200}});
Expand All @@ -164,6 +185,17 @@ describe('findPackageForDownload', () => {
expect(spyHttpClientGet).toHaveBeenCalledTimes(1);
});

it('always resolves major-only requests remotely', () => {
expect(distribution['requiresRemoteResolution']()).toBe(true);
const exactDistribution = new OracleDistribution({
version: '21.0.8',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
expect(exactDistribution['requiresRemoteResolution']()).toBe(false);
});

it.each([
['amd64', 'x64'],
['arm64', 'aarch64']
Expand Down
Loading
Loading