Skip to content

Commit

Permalink
Resolve min as the earliest compatible Julia version (compatible wi…
Browse files Browse the repository at this point in the history
…th the user's project) (#202)

* Support the special version "MIN"

* Support JULIA_PROJECT

* Add tests

* Add forgotten test fixtures

* Get latest prerelease/release

* No special pre-release behaviour

* Add test for NPM semver difference

* Robust test suite

* Disallow less-than-equal

* Refactor validJuliaCompatRange to return a validRange

* Rename MIN to min

* Rename getProjectFile to getProjectFilePath

* Comment on "project" input

* Additional tests for getProjectFilePath

* Add comment on `juliaCompatRange`

Co-authored-by: Dilum Aluthge <dilum@aluthge.com>

* Update dependencies

---------

Co-authored-by: Dilum Aluthge <dilum@aluthge.com>
  • Loading branch information
omus and DilumAluthge authored Aug 30, 2024
1 parent b83c8a2 commit 014c323
Show file tree
Hide file tree
Showing 13 changed files with 1,791 additions and 4,139 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ This action sets up a Julia environment for use in actions by downloading a spec
#
# Default: false
show-versioninfo: ''

# Set the path to the project directory or file to use when resolving some versions (e.g. `min`).
#
# Defaults to using JULIA_PROJECT if defined, otherwise '.'
project: ''
```
### Outputs
Expand Down Expand Up @@ -121,6 +126,7 @@ You can either specify specific Julia versions or version ranges. If you specify
- `'pre'` will install the latest prerelease build (RCs, betas, and alphas).
- `'nightly'` will install the latest nightly build.
- `'1.7-nightly'` will install the latest nightly build for the upcoming 1.7 release. This version will only be available during certain phases of the Julia release cycle.
- `'min'` will install the earliest supported version of Julia compatible with the project. Especially useful in monorepos.

Internally the action uses node's semver package to resolve version ranges. Its [documentation](https://github.com/npm/node-semver#advanced-range-syntax) contains more details on the version range syntax. You can test what version will be selected for a given input in this JavaScript [REPL](https://repl.it/@SaschaMann/setup-julia-version-logic).

Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
157 changes: 157 additions & 0 deletions __tests__/installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,135 @@ process.env['RUNNER_TOOL_CACHE'] = toolDir
process.env['RUNNER_TEMP'] = tempDir

import * as installer from '../src/installer'
import exp from 'constants'

describe("getProjectFilePath tests", () => {
let orgJuliaProject
let orgWorkingDir

beforeEach(() => {
orgJuliaProject = process.env["JULIA_PROJECT"]
orgWorkingDir = process.cwd()
delete process.env["JULIA_PROJECT"]
})

afterEach(() => {
process.env["JULIA_PROJECT"] = orgJuliaProject
process.chdir(orgWorkingDir)
})

it("Can determine project file is missing", () => {
expect(() => installer.getProjectFilePath("DNE.toml")).toThrow("Unable to locate project file")
expect(() => installer.getProjectFilePath(fixtureDir)).toThrow("Unable to locate project file")
expect(() => installer.getProjectFilePath()).toThrow("Unable to locate project file")
})

it('Can determine project file from a directory', () => {
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgA"))).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgB"))).toEqual(path.join(fixtureDir, "PkgB", "JuliaProject.toml"))
})

it("Prefers using JuliaProject.toml over Project.toml", () => {
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgC"))).toEqual(path.join(fixtureDir, "PkgC", "JuliaProject.toml"))
})

it("Can determine project from JULIA_PROJECT", () => {
process.env["JULIA_PROJECT"] = path.join(fixtureDir, "PkgA")
expect(installer.getProjectFilePath()).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
})

it("Can determine project from the current working directory", () => {
process.chdir(path.join(fixtureDir, "PkgA"));
expect(installer.getProjectFilePath()).toEqual("Project.toml")
})

it("Ignores JULIA_PROJECT when argument is used", () => {
process.env["JULIA_PROJECT"] = path.join(fixtureDir, "PkgB")
expect(installer.getProjectFilePath(path.join(fixtureDir, "PkgA"))).toEqual(path.join(fixtureDir, "PkgA", "Project.toml"))
})
})

describe("validJuliaCompatRange tests", () => {
it('Handles default caret specifier', () => {
expect(installer.validJuliaCompatRange("1")).toEqual(semver.validRange("^1"))
expect(installer.validJuliaCompatRange("1.2")).toEqual(semver.validRange("^1.2"))
expect(installer.validJuliaCompatRange("1.2.3")).toEqual(semver.validRange("^1.2.3"))

// TODO: Pkg.jl currently does not support pre-release entries in compat so ideally this would fail
expect(installer.validJuliaCompatRange("1.2.3-rc1")).toEqual(semver.validRange("^1.2.3-rc1"))
})

it('Handle surrounding whitespace', () => {
expect(installer.validJuliaCompatRange(" 1")).toEqual(semver.validRange("^1"))
expect(installer.validJuliaCompatRange("1 ")).toEqual(semver.validRange("^1"))
expect(installer.validJuliaCompatRange(" 1 ")).toEqual(semver.validRange("^1"))
})

it('Handles version ranges with specifiers', () => {
expect(installer.validJuliaCompatRange("^1.2.3")).toEqual(semver.validRange("^1.2.3"))
expect(installer.validJuliaCompatRange("~1.2.3")).toEqual(semver.validRange("~1.2.3"))
expect(installer.validJuliaCompatRange("=1.2.3")).toEqual(semver.validRange("=1.2.3"))
expect(installer.validJuliaCompatRange(">=1.2.3")).toEqual(">=1.2.3")
expect(installer.validJuliaCompatRange("≥1.2.3")).toEqual(">=1.2.3")
expect(installer.validJuliaCompatRange("<1.2.3")).toEqual("<1.2.3")
})

it('Handles whitespace after specifiers', () => {
expect(installer.validJuliaCompatRange("^ 1.2.3")).toBeNull()
expect(installer.validJuliaCompatRange("~ 1.2.3")).toBeNull()
expect(installer.validJuliaCompatRange("= 1.2.3")).toBeNull()
expect(installer.validJuliaCompatRange(">= 1.2.3")).toEqual(">=1.2.3")
expect(installer.validJuliaCompatRange("≥ 1.2.3")).toEqual(">=1.2.3")
expect(installer.validJuliaCompatRange("< 1.2.3")).toEqual("<1.2.3")
})

it('Handles hypen ranges', () => {
expect(installer.validJuliaCompatRange("1.2.3 - 4.5.6")).toEqual(semver.validRange("1.2.3 - 4.5.6"))
expect(installer.validJuliaCompatRange("1.2.3-rc1 - 4.5.6")).toEqual(semver.validRange("1.2.3-rc1 - 4.5.6"))
expect(installer.validJuliaCompatRange("1.2.3-rc1-4.5.6")).toEqual(semver.validRange("^1.2.3-rc1-4.5.6")) // A version number and not a hypen range
expect(installer.validJuliaCompatRange("1.2.3-rc1 -4.5.6")).toBeNull()
expect(installer.validJuliaCompatRange("1.2.3-rc1- 4.5.6")).toBeNull() // Whitespace separate version ranges
})

it("Returns null AND operator on version ranges", () => {
expect(installer.validJuliaCompatRange("")).toBeNull()
expect(installer.validJuliaCompatRange("1 2 3")).toBeNull()
expect(installer.validJuliaCompatRange("1- 2")).toBeNull()
expect(installer.validJuliaCompatRange("<1 <1")).toBeNull()
expect(installer.validJuliaCompatRange("< 1 < 1")).toBeNull()
expect(installer.validJuliaCompatRange("< 1 < 1")).toBeNull()
})

it('Returns null with invalid specifiers', () => {
expect(installer.validJuliaCompatRange("<=1.2.3")).toBeNull()
expect(installer.validJuliaCompatRange("≤1.2.3")).toBeNull()
expect(installer.validJuliaCompatRange("*")).toBeNull()
})

it("Handles OR operator on version ranges", () => {
expect(installer.validJuliaCompatRange("1, 2, 3")).toEqual(semver.validRange("^1 || ^2 || ^3"))
expect(installer.validJuliaCompatRange("1, 2 - 3, ≥ 4")).toEqual(semver.validRange("^1 || >=2 <=3 || >=4"))
expect(installer.validJuliaCompatRange(",")).toBeNull()
})
})

describe("readJuliaCompatRange tests", () => {
it('Can determine Julia compat entries', () => {
const toml = '[compat]\njulia = "1, ^1.1, ~1.2, >=1.3, 1.4 - 1.5"'
expect(installer.readJuliaCompatRange(toml)).toEqual(semver.validRange("^1 || ^1.1 || ~1.2 || >=1.3 || 1.4 - 1.5"))
})

it('Throws with invalid version ranges', () => {
expect(() => installer.readJuliaCompatRange('[compat]\njulia = ""')).toThrow("Invalid version range")
expect(() => installer.readJuliaCompatRange('[compat]\njulia = "1 2 3"')).toThrow("Invalid version range")
})

it('Handle missing compat entries', () => {
expect(installer.readJuliaCompatRange("")).toEqual("*")
expect(installer.readJuliaCompatRange("[compat]")).toEqual("*")

})
})

describe('version matching tests', () => {
describe('specific versions', () => {
Expand Down Expand Up @@ -95,6 +224,34 @@ describe('version matching tests', () => {
})
})
})

describe('julia compat versions', () => {
it('Understands "min"', () => {
let versions = ["1.6.7", "1.7.1-rc1", "1.7.1-rc2", "1.7.1", "1.7.2", "1.8.0"]
expect(installer.getJuliaVersion(versions, "min", false, "^1.7")).toEqual("1.7.1")
expect(installer.getJuliaVersion(versions, "min", true, "^1.7")).toEqual("1.7.1-rc1")

versions = ["1.6.7", "1.7.3-rc1", "1.7.3-rc2", "1.8.0"]
expect(installer.getJuliaVersion(versions, "min", false, "^1.7")).toEqual("1.8.0")
expect(installer.getJuliaVersion(versions, "min", true, "^1.7")).toEqual("1.7.3-rc1")

expect(installer.getJuliaVersion(versions, "min", false, "~1.7 || ~1.8 || ~1.9")).toEqual("1.8.0")
expect(installer.getJuliaVersion(versions, "min", true, "~1.7 || ~1.8 || ~1.9")).toEqual("1.7.3-rc1")
expect(installer.getJuliaVersion(versions, "min", false, "~1.8 || ~1.7 || ~1.9")).toEqual("1.8.0")
expect(installer.getJuliaVersion(versions, "min", true, "~1.8 || ~1.7 || ~1.9")).toEqual("1.7.3-rc1")

expect(installer.getJuliaVersion(versions, "min", false, "1.7 - 1.9")).toEqual("1.8.0")
expect(installer.getJuliaVersion(versions, "min", true, "1.7 - 1.9")).toEqual("1.7.3-rc1")

expect(installer.getJuliaVersion(versions, "min", true, "< 1.9.0")).toEqual("1.6.7")
expect(installer.getJuliaVersion(versions, "min", true, ">= 1.6.0")).toEqual("1.6.7")

// NPM's semver package treats "1.7" as "~1.7" instead of "^1.7" like Julia
expect(() => installer.getJuliaVersion(versions, "min", false, "1.7")).toThrow("Could not find a Julia version that matches")

expect(() => installer.getJuliaVersion(versions, "min", true, "")).toThrow("Julia project file does not specify a compat for Julia")
})
})
})

describe('installer tests', () => {
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ inputs:
description: 'Display InteractiveUtils.versioninfo() after installing'
required: false
default: 'false'
project:
description: 'The path to the project directory or file to use when resolving some versions (e.g. min)'
required: false
default: '' # Special value which fallsback to using JULIA_PROJECT if defined, otherwise "."
outputs:
julia-version:
description: 'The installed Julia version. May vary from the version input if a version range was given as input.'
Expand Down
108 changes: 99 additions & 9 deletions lib/installer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion lib/setup-julia.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 014c323

Please sign in to comment.