From acd318a1d94d3ebddf8153debf06dfdd1bacb7dd Mon Sep 17 00:00:00 2001 From: Sergio Zharinov Date: Mon, 24 Jun 2019 20:43:48 +0400 Subject: [PATCH] feat(swift): Support for Package.swift files (#3911) --- lib/config/definitions.js | 14 + lib/datasource/git-tags/index.js | 2 +- lib/manager/index.js | 1 + lib/manager/swift/extract.js | 346 ++++++ lib/manager/swift/index.js | 7 + lib/manager/swift/update.js | 30 + lib/versioning/swift/index.js | 48 + lib/versioning/swift/range.js | 58 + renovate-schema.json | 13 +- .../__snapshots__/validation.spec.js.snap | 2 +- test/datasource/git-tags.spec.js | 13 +- .../swift/__snapshots__/index.spec.js.snap | 134 ++ .../swift/_fixtures/SamplePackage.swift | 1105 +++++++++++++++++ test/manager/swift/index.spec.js | 203 +++ test/versioning/swift.spec.js | 85 ++ .../extract/__snapshots__/index.spec.js.snap | 3 + website/docs/configuration-options.md | 16 + 17 files changed, 2073 insertions(+), 7 deletions(-) create mode 100644 lib/manager/swift/extract.js create mode 100644 lib/manager/swift/index.js create mode 100644 lib/manager/swift/update.js create mode 100644 lib/versioning/swift/index.js create mode 100644 lib/versioning/swift/range.js create mode 100644 test/manager/swift/__snapshots__/index.spec.js.snap create mode 100644 test/manager/swift/_fixtures/SamplePackage.swift create mode 100644 test/manager/swift/index.spec.js create mode 100644 test/versioning/swift.spec.js diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 600bac44fe..91c888981f 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -438,6 +438,7 @@ const options = [ 'poetry', 'ruby', 'semver', + 'swift', ], default: 'semver', cli: false, @@ -1811,6 +1812,19 @@ const options = [ env: false, mergeable: true, }, + { + name: 'swift', + description: 'Configuration for Package.swift files', + stage: 'package', + type: 'object', + default: { + fileMatch: ['(^|/)Package\\.swift'], + versionScheme: 'swift', + rangeStrategy: 'bump', + }, + mergeable: true, + cli: false, + }, ]; function getOptions() { diff --git a/lib/datasource/git-tags/index.js b/lib/datasource/git-tags/index.js index 7baf90a690..7d2e3ff0c7 100644 --- a/lib/datasource/git-tags/index.js +++ b/lib/datasource/git-tags/index.js @@ -6,7 +6,7 @@ const cacheMinutes = 10; async function getPkgReleases({ lookupName }) { try { const cachedResult = await renovateCache.get(cacheNamespace, lookupName); - // istanbul ignore if + /* istanbul ignore next line */ if (cachedResult) return cachedResult; const info = await getRemoteInfo({ diff --git a/lib/manager/index.js b/lib/manager/index.js index ad1bb4dd42..90b3cdde18 100644 --- a/lib/manager/index.js +++ b/lib/manager/index.js @@ -27,6 +27,7 @@ const managerList = [ 'poetry', 'pub', 'sbt', + 'swift', 'terraform', 'travis', 'ruby-version', diff --git a/lib/manager/swift/extract.js b/lib/manager/swift/extract.js new file mode 100644 index 0000000000..bc5cbb9dce --- /dev/null +++ b/lib/manager/swift/extract.js @@ -0,0 +1,346 @@ +const { isValid } = require('../../versioning/swift'); + +const regExps = { + wildcard: /^.*?/, + space: /(\s+|\/\/[^\n]*|\/\*.*\*\/)+/s, + depsKeyword: /dependencies/, + colon: /:/, + beginSection: /\[/, + endSection: /],?/, + package: /\s*.\s*package\s*\(\s*/, + urlKey: /url/, + stringLiteral: /"[^"]+"/, + comma: /,/, + from: /from/, + rangeOp: /\.\.[.<]/, + exactVersion: /\.\s*exact\s*\(\s*/, +}; + +const WILDCARD = 'wildcard'; +const SPACE = 'space'; +const DEPS = 'depsKeyword'; +const COLON = 'colon'; +const BEGIN_SECTION = 'beginSection'; +const END_SECTION = 'endSection'; +const PACKAGE = 'package'; +const URL_KEY = 'urlKey'; +const STRING_LITERAL = 'stringLiteral'; +const COMMA = 'comma'; +const FROM = 'from'; +const RANGE_OP = 'rangeOp'; +const EXACT_VERSION = 'exactVersion'; + +const searchLabels = { + wildcard: WILDCARD, + space: SPACE, + depsKeyword: DEPS, + colon: COLON, + beginSection: BEGIN_SECTION, + endSection: END_SECTION, + package: PACKAGE, + urlKey: URL_KEY, + stringLiteral: STRING_LITERAL, + comma: COMMA, + from: FROM, + rangeOp: RANGE_OP, + exactVersion: EXACT_VERSION, +}; + +function searchKeysForState(state) { + switch (state) { + case 'dependencies': + return [SPACE, COLON, WILDCARD]; + case 'dependencies:': + return [SPACE, BEGIN_SECTION, WILDCARD]; + case 'dependencies: [': + return [SPACE, PACKAGE, END_SECTION]; + case '.package(': + return [SPACE, URL_KEY, PACKAGE, END_SECTION]; + case '.package(url': + return [SPACE, COLON, PACKAGE, END_SECTION]; + case '.package(url:': + return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION]; + case '.package(url: [depName]': + return [SPACE, COMMA, PACKAGE, END_SECTION]; + case '.package(url: [depName],': + return [ + SPACE, + FROM, + STRING_LITERAL, + RANGE_OP, + EXACT_VERSION, + PACKAGE, + END_SECTION, + ]; + case '.package(url: [depName], .exact(': + return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION]; + case '.package(url: [depName], from': + return [SPACE, COLON, PACKAGE, END_SECTION]; + case '.package(url: [depName], from:': + return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION]; + case '.package(url: [depName], [value]': + return [SPACE, RANGE_OP, PACKAGE, END_SECTION]; + case '.package(url: [depName], [rangeFrom][rangeOp]': + return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION]; + default: + return [DEPS]; + } +} + +function getMatch(str, state) { + const keys = searchKeysForState(state); + let result = null; + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + const regex = regExps[key]; + const label = searchLabels[key]; + const match = str.match(regex); + if (match) { + const idx = match.index; + const substr = match[0]; + const len = substr.length; + if (idx === 0) { + return { idx, len, label, substr }; + } + if (!result || idx < result.idx) { + result = { idx, len, label, substr }; + } + } + } + return result; +} + +function getDepName(url) { + try { + const { host, pathname } = new URL(url); + if (host === 'github.com' || host === 'gitlab.com') { + return pathname + .replace(/^\//, '') + .replace(/\.git$/, '') + .replace(/\/$/, ''); + } + return url; + } catch (e) { + return null; + } +} + +function extractPackageFile(content, packageFile = null) { + if (!content) return null; + + const result = { + packageFile, + }; + const deps = []; + + let offset = 0; + let restStr = content; + let state = null; + let match = getMatch(restStr, state); + + let lookupName = null; + let currentValue = null; + let fileReplacePosition = null; + + function yieldDep() { + const depName = getDepName(lookupName); + if (depName && currentValue && fileReplacePosition) { + const dep = { + datasource: 'gitTags', + depName, + lookupName, + currentValue, + fileReplacePosition, + }; + + if (isValid(currentValue)) { + deps.push(dep); + } + } + lookupName = null; + currentValue = null; + fileReplacePosition = null; + } + + while (match) { + const { idx, len, label, substr } = match; + offset += idx; + // eslint-disable-next-line default-case + switch (state) { + case null: + if (deps.length) break; + if (label === DEPS) { + state = 'dependencies'; + } + break; + case 'dependencies': + if (label === COLON) { + state = 'dependencies:'; + } else if (label !== SPACE) { + state = null; + } + break; + case 'dependencies:': + if (label === BEGIN_SECTION) { + state = 'dependencies: ['; + } else if (label !== SPACE) { + state = null; + } + break; + case 'dependencies: [': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === URL_KEY) { + state = '.package(url'; + } else if (label === PACKAGE) { + yieldDep(); + } + break; + case '.package(url': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === COLON) { + state = '.package(url:'; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url:': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === STRING_LITERAL) { + lookupName = substr.replace(/^"/, '').replace(/"$/, ''); + state = '.package(url: [depName]'; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName]': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === COMMA) { + state = '.package(url: [depName],'; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName],': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === FROM) { + fileReplacePosition = offset; + currentValue = substr; + state = '.package(url: [depName], from'; + } else if (label === STRING_LITERAL) { + fileReplacePosition = offset; + currentValue = substr; + state = '.package(url: [depName], [value]'; + } else if (label === RANGE_OP) { + fileReplacePosition = offset; + currentValue = substr; + state = '.package(url: [depName], [rangeFrom][rangeOp]'; + } else if (label === EXACT_VERSION) { + state = '.package(url: [depName], .exact('; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName], .exact(': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === STRING_LITERAL) { + currentValue = substr.slice(1, substr.length - 1); + fileReplacePosition = offset; + yieldDep(); + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName], from': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === COLON) { + currentValue += substr; + state = '.package(url: [depName], from:'; + } else if (label === SPACE) { + currentValue += substr; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName], from:': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === STRING_LITERAL) { + currentValue += substr; + yieldDep(); + state = 'dependencies: ['; + } else if (label === SPACE) { + currentValue += substr; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName], [value]': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === RANGE_OP) { + currentValue += substr; + state = '.package(url: [depName], [rangeFrom][rangeOp]'; + } else if (label === SPACE) { + currentValue += substr; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + case '.package(url: [depName], [rangeFrom][rangeOp]': + if (label === END_SECTION) { + yieldDep(); + state = null; + } else if (label === STRING_LITERAL) { + currentValue += substr; + state = 'dependencies: ['; + } else if (label === SPACE) { + currentValue += substr; + } else if (label === PACKAGE) { + yieldDep(); + state = '.package('; + } + break; + } + offset += len; + restStr = restStr.slice(idx + len); + match = getMatch(restStr, state); + } + return deps.length ? { ...result, deps } : null; +} + +module.exports = { + extractPackageFile, +}; diff --git a/lib/manager/swift/index.js b/lib/manager/swift/index.js new file mode 100644 index 0000000000..749cae6f4d --- /dev/null +++ b/lib/manager/swift/index.js @@ -0,0 +1,7 @@ +const { extractPackageFile } = require('./extract'); +const { updateDependency } = require('./update'); + +module.exports = { + extractPackageFile, + updateDependency, +}; diff --git a/lib/manager/swift/update.js b/lib/manager/swift/update.js new file mode 100644 index 0000000000..09b2baa68c --- /dev/null +++ b/lib/manager/swift/update.js @@ -0,0 +1,30 @@ +const { isVersion } = require('../../versioning/swift'); + +const fromParam = /^\s*from\s*:\s*"([^"]+)"\s*$/; + +function updateDependency(fileContent, upgrade) { + const { currentValue, newValue, fileReplacePosition } = upgrade; + const leftPart = fileContent.slice(0, fileReplacePosition); + const rightPart = fileContent.slice(fileReplacePosition); + const oldVal = isVersion(currentValue) ? `"${currentValue}"` : currentValue; + let newVal; + if (fromParam.test(oldVal)) { + const [, version] = oldVal.match(fromParam); + newVal = oldVal.replace(version, newValue); + } else if (isVersion(newValue)) { + newVal = `"${newValue}"`; + } else { + newVal = newValue; + } + if (rightPart.indexOf(oldVal) === 0) { + return leftPart + rightPart.replace(oldVal, newVal); + } + if (rightPart.indexOf(newVal) === 0) { + return fileContent; + } + return null; +} + +module.exports = { + updateDependency, +}; diff --git a/lib/versioning/swift/index.js b/lib/versioning/swift/index.js new file mode 100644 index 0000000000..5d6934066c --- /dev/null +++ b/lib/versioning/swift/index.js @@ -0,0 +1,48 @@ +const semver = require('semver'); +const stable = require('semver-stable'); +const { toSemverRange, getNewValue } = require('./range'); + +const { is: isStable } = stable; + +const { + compare: sortVersions, + maxSatisfying, + minSatisfying, + major: getMajor, + minor: getMinor, + patch: getPatch, + satisfies, + valid, + validRange, + ltr, + gt: isGreaterThan, + eq: equals, +} = semver; + +const isValid = input => !!valid(input) || !!validRange(toSemverRange(input)); +const isVersion = input => !!valid(input); +const maxSatisfyingVersion = (versions, range) => + maxSatisfying(versions, toSemverRange(range)); +const minSatisfyingVersion = (versions, range) => + minSatisfying(versions, toSemverRange(range)); +const isLessThanRange = (version, range) => ltr(version, toSemverRange(range)); +const matches = (version, range) => satisfies(version, toSemverRange(range)); + +module.exports = { + equals, + getMajor, + getMinor, + getNewValue, + getPatch, + isCompatible: isVersion, + isGreaterThan, + isLessThanRange, + isSingleVersion: isVersion, + isStable, + isValid, + isVersion, + matches, + maxSatisfyingVersion, + minSatisfyingVersion, + sortVersions, +}; diff --git a/lib/versioning/swift/range.js b/lib/versioning/swift/range.js new file mode 100644 index 0000000000..4b9e3f36b9 --- /dev/null +++ b/lib/versioning/swift/range.js @@ -0,0 +1,58 @@ +const semver = require('semver'); + +const fromParam = /^\s*from\s*:\s*"([^"]+)"\s*$/; +const fromRange = /^\s*"([^"]+)"\s*\.\.\.\s*$/; +const binaryRange = /^\s*"([^"]+)"\s*(\.\.[.<])\s*"([^"]+)"\s*$/; +const toRange = /^\s*(\.\.[.<])\s*"([^"]+)"\s*$/; + +function toSemverRange(range) { + if (fromParam.test(range)) { + const [, version] = range.match(fromParam); + if (semver.valid(version)) { + const nextMajor = `${semver.major(version) + 1}.0.0`; + return `>=${version} <${nextMajor}`; + } + } else if (fromRange.test(range)) { + const [, version] = range.match(fromRange); + if (semver.valid(version)) { + return `>=${version}`; + } + } else if (binaryRange.test(range)) { + const [, fromVersion, op, toVersion] = range.match(binaryRange); + if (semver.valid(fromVersion) && semver.valid(toVersion)) { + return op === '..<' + ? `>=${fromVersion} <${toVersion}` + : `>=${fromVersion} <=${toVersion}`; + } + } else if (toRange.test(range)) { + const [, op, toVersion] = range.match(toRange); + if (semver.valid(toVersion)) { + return op === '..<' ? `<${toVersion}` : `<=${toVersion}`; + } + } + return null; +} + +function getNewValue(currentValue, rangeStrategy, fromVersion, toVersion) { + if (fromParam.test(currentValue)) { + return toVersion; + } + if (fromRange.test(currentValue)) { + const [, version] = currentValue.match(fromRange); + return currentValue.replace(version, toVersion); + } + if (binaryRange.test(currentValue)) { + const [, , , version] = currentValue.match(binaryRange); + return currentValue.replace(version, toVersion); + } + if (toRange.test(currentValue)) { + const [, , version] = currentValue.match(toRange); + return currentValue.replace(version, toVersion); + } + return currentValue; +} + +module.exports = { + toSemverRange, + getNewValue, +}; diff --git a/renovate-schema.json b/renovate-schema.json index 1e7259cea4..a97ff59832 100644 --- a/renovate-schema.json +++ b/renovate-schema.json @@ -290,7 +290,8 @@ "pep440", "poetry", "ruby", - "semver" + "semver", + "swift" ], "default": "semver" }, @@ -1241,6 +1242,16 @@ "description": "Options to suppress various types of warnings and other notifications", "type": "array", "default": ["deprecationWarningIssues"] + }, + "swift": { + "description": "Configuration for Package.swift files", + "type": "object", + "default": { + "fileMatch": ["(^|/)Package\\.swift"], + "versionScheme": "swift", + "rangeStrategy": "bump" + }, + "$ref": "#" } } } diff --git a/test/config/__snapshots__/validation.spec.js.snap b/test/config/__snapshots__/validation.spec.js.snap index be2e63073f..ed19fc38fd 100644 --- a/test/config/__snapshots__/validation.spec.js.snap +++ b/test/config/__snapshots__/validation.spec.js.snap @@ -87,7 +87,7 @@ Array [ "depName": "Configuration Error", "message": "packageRules: You have included an unsupported manager in a package rule. Your list: foo. - Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, github-actions, gitlabci, gomod, gradle, gradle-wrapper, kubernetes, leiningen, maven, meteor, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, terraform, travis, ruby-version, homebrew).", + Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, github-actions, gitlabci, gomod, gradle, gradle-wrapper, kubernetes, leiningen, maven, meteor, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version, homebrew).", }, ] `; diff --git a/test/datasource/git-tags.spec.js b/test/datasource/git-tags.spec.js index eaca9b0ad2..d9b389e4f4 100644 --- a/test/datasource/git-tags.spec.js +++ b/test/datasource/git-tags.spec.js @@ -3,21 +3,23 @@ const { getPkgReleases } = require('../../lib/datasource/git-tags'); jest.mock('isomorphic-git'); -const lookupName = 'https://github.com/vapor/vapor.git'; +const lookupName = 'vapor'; +const registryUrls = ['https://github.com/vapor/vapor.git']; +const registryUrlsAlt = ['https://github.com/vapor/vapor/']; describe('datasource/git-tags', () => { beforeEach(() => global.renovateCache.rmAll()); describe('getPkgReleases', () => { it('returns nil if response is wrong', async () => { getRemoteInfo.mockReturnValue(Promise.resolve(null)); - const versions = await getPkgReleases({ lookupName }); + const versions = await getPkgReleases({ lookupName, registryUrls }); expect(versions).toEqual(null); }); it('returns nil if remote call throws exception', async () => { getRemoteInfo.mockImplementation(() => { throw new Error(); }); - const versions = await getPkgReleases({ lookupName }); + const versions = await getPkgReleases({ lookupName, registryUrls }); expect(versions).toEqual(null); }); it('returns versions filtered from tags', async () => { @@ -32,7 +34,10 @@ describe('datasource/git-tags', () => { }, }) ); - const versions = await getPkgReleases({ lookupName }); + const versions = await getPkgReleases({ + lookupName, + registryUrls: registryUrlsAlt, + }); const result = versions.releases.map(x => x.version).sort(); expect(result).toEqual(['0.0.1', '0.0.2']); }); diff --git a/test/manager/swift/__snapshots__/index.spec.js.snap b/test/manager/swift/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000000..9605f6e5ab --- /dev/null +++ b/test/manager/swift/__snapshots__/index.spec.js.snap @@ -0,0 +1,134 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lib/manager/swift extractPackageFile() parses multiple packages 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "0.1.0", + "datasource": "gitTags", + "depName": "avito-tech/GraphiteClient", + "fileReplacePosition": 1177, + "lookupName": "https://github.com/avito-tech/GraphiteClient.git", + }, + Object { + "currentValue": "1.0.16", + "datasource": "gitTags", + "depName": "IBM-Swift/BlueSignals", + "fileReplacePosition": 1268, + "lookupName": "https://github.com/IBM-Swift/BlueSignals.git", + }, + Object { + "currentValue": "3.0.6", + "datasource": "gitTags", + "depName": "daltoniam/Starscream", + "fileReplacePosition": 1439, + "lookupName": "https://github.com/daltoniam/Starscream.git", + }, + Object { + "currentValue": "1.4.6", + "datasource": "gitTags", + "depName": "httpswift/swifter", + "fileReplacePosition": 1523, + "lookupName": "https://github.com/httpswift/swifter.git", + }, + Object { + "currentValue": "from : \\"0.9.6\\"", + "datasource": "gitTags", + "depName": "weichsel/ZIPFoundation", + "fileReplacePosition": 1626, + "lookupName": "https://github.com/weichsel/ZIPFoundation/", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "from:\\"1.2.3\\"", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 2`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "\\"1.2.3\\"...", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 3`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "\\"1.2.3\\"...\\"1.2.4\\"", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 4`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "\\"1.2.3\\"..<\\"1.2.4\\"", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 5`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "...\\"1.2.3\\"", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; + +exports[`lib/manager/swift extractPackageFile() parses package descriptions 6`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "..<\\"1.2.3\\"", + "datasource": "gitTags", + "depName": "vapor/vapor", + "fileReplacePosition": 64, + "lookupName": "https://github.com/vapor/vapor.git", + }, + ], + "packageFile": null, +} +`; diff --git a/test/manager/swift/_fixtures/SamplePackage.swift b/test/manager/swift/_fixtures/SamplePackage.swift new file mode 100644 index 0000000000..2c8e2a13d0 --- /dev/null +++ b/test/manager/swift/_fixtures/SamplePackage.swift @@ -0,0 +1,1105 @@ +// swift-tools-version:4.2 + +import PackageDescription + +let package = Package( + name: "AvitoUITestRunner", + products: [ + // MARK: - Products + .executable( + // MARK: AvitoRunner + name: "AvitoRunner", + targets: [ + "AvitoRunner" + ] + ), + .library( + // MARK: EmceePlugin + name: "EmceePlugin", + targets: [ + "Models", + "Logging", + "Plugin" + ] + ), + .executable( + // MARK: fake_fbxctest + name: "fake_fbxctest", + targets: ["FakeFbxctest"] + ), + .executable( + // MARK: testing_plugin + name: "testing_plugin", + targets: ["TestingPlugin"] + ) + ], + dependencies : [ + // MARK: - Dependencies + .package(url: "https://github.com/0x7fs/CountedSet", .branch("master")), + .package(url: "foo", .exact("1.2.3.4")), + .package(url: "bar", "1.2.3.4.5"...), + .package(url: "baz", from: "1.2.3.4"), + .package(url: "https://github.com/avito-tech/GraphiteClient.git", .exact( "0.1.0" )), + .package(url: "https://github.com/IBM-Swift/BlueSignals.git", .exact("1.0.16")), + .package(url: "https://github.com/beefon/Shout", .branch("UpdateSocket")), + .package(url: "https://github.com/daltoniam/Starscream.git", .exact("3.0.6")), + .package(url: "https://github.com/httpswift/swifter.git", .exact("1.4.6")), + . package ( url : "https://github.com/weichsel/ZIPFoundation/" , + /*foobar*/ from : "0.9.6") + .package(url: "https://github.com/apple/swift-package-manager.git", .branch("swift-5.0-branch")), + ], + targets: [ + // MARK: - Targets + .target( + // MARK: Ansi + name: "Ansi", + dependencies: [ + ] + ), + .target( + // MARK: ArgumentsParser + name: "ArgumentsParser", + dependencies: [ + "Extensions", + "Logging", + "Models", + "RuntimeDump", + "SPMUtility" + ] + ), + .target( + // MARK: AutomaticTermination + name: "AutomaticTermination", + dependencies: [ + "DateProvider", + "Logging", + "Timer" + ] + ), + .testTarget( + // MARK: AutomaticTerminationTests + name: "AutomaticTerminationTests", + dependencies: [ + "AutomaticTermination", + "DateProvider" + ] + ), + .target( + // MARK: AvitoRunner + name: "AvitoRunner", + dependencies: [ + "AvitoRunnerLib" + ] + ), + .target( + // MARK: AvitoRunnerLib + name: "AvitoRunnerLib", + dependencies: [ + "ArgumentsParser", + "ChromeTracing", + "Deployer", + "DistRunner", + "DistWorker", + "EventBus", + "JunitReporting", + "LaunchdUtils", + "LocalQueueServerRunner", + "LoggingSetup", + "Metrics", + "Models", + "PluginManager", + "PortDeterminer", + "ProcessController", + "RemoteQueue", + "ResourceLocationResolver", + "SSHDeployer", + "ScheduleStrategy", + "Scheduler", + "SignalHandling", + "SPMUtility", + "Version" + ] + ), + .testTarget( + // MARK: AvitoRunnerLibTests + name: "AvitoRunnerLibTests", + dependencies: [ + "AvitoRunnerLib", + "Models", + "ModelsTestHelpers" + ] + ), + .target( + // MARK: BalancingBucketQueue + name: "BalancingBucketQueue", + dependencies: [ + "BucketQueue", + "DateProvider", + "Logging", + "Models", + "ResultsCollector", + "SPMUtility" + ] + ), + .testTarget( + // MARK: BalancingBucketQueueTests + name: "BalancingBucketQueueTests", + dependencies: [ + "BalancingBucketQueue", + "BucketQueueTestHelpers", + "ResultsCollector" + ] + ), + .target( + // MARK: BucketQueue + name: "BucketQueue", + dependencies: [ + "DateProvider", + "Logging", + "Models", + "WorkerAlivenessTracker" + ] + ), + .target( + // MARK: BucketQueueTestHelpers + name: "BucketQueueTestHelpers", + dependencies: [ + "BucketQueue", + "DateProviderTestHelpers", + "Models", + "ModelsTestHelpers", + "WorkerAlivenessTracker" + ] + ), + .testTarget( + // MARK: BucketQueueTests + name: "BucketQueueTests", + dependencies: [ + "BucketQueue", + "BucketQueueTestHelpers", + "DateProviderTestHelpers", + "ModelsTestHelpers", + "WorkerAlivenessTrackerTestHelpers" + ] + ), + .target( + // MARK: ChromeTracing + name: "ChromeTracing", + dependencies: [ + "Models" + ] + ), + .target( + // MARK: CurrentlyBeingProcessedBucketsTracker + name: "CurrentlyBeingProcessedBucketsTracker", + dependencies: [ + "CountedSet" + ] + ), + .testTarget( + // MARK: CurrentlyBeingProcessedBucketsTrackerTests + name: "CurrentlyBeingProcessedBucketsTrackerTests", + dependencies: [ + "CurrentlyBeingProcessedBucketsTracker" + ] + ), + .target( + // MARK: DateProvider + name: "DateProvider", + dependencies: [] + ), + .target( + // MARK: DateProviderTestHelpers + name: "DateProviderTestHelpers", + dependencies: [ + "DateProvider" + ] + ), + .target( + // MARK: Deployer + name: "Deployer", + dependencies: [ + "Extensions", + "Logging", + "Models", + "SPMUtility", + "ZIPFoundation" + ] + ), + .testTarget( + // MARK: DeployerTests + name: "DeployerTests", + dependencies: [ + "Deployer" + ] + ), + .target( + // MARK: DistDeployer + name: "DistDeployer", + dependencies: [ + "Deployer", + "LaunchdUtils", + "Logging", + "Models", + "SSHDeployer", + "TempFolder", + "Version" + ] + ), + .testTarget( + // MARK: DistDeployerTests + name: "DistDeployerTests", + dependencies: [ + "Deployer", + "DistDeployer", + "Extensions", + "Models", + "ModelsTestHelpers", + "ResourceLocationResolver", + "TempFolder", + "SPMUtility" + ] + ), + .target( + // MARK: DistRunner + name: "DistRunner", + dependencies: [ + "AutomaticTermination", + "BucketQueue", + "DateProvider", + "DistDeployer", + "EventBus", + "Extensions", + "LocalHostDeterminer", + "Models", + "PortDeterminer", + "QueueServer", + "ResourceLocationResolver", + "ScheduleStrategy", + "TempFolder", + "Version" + ] + ), + .testTarget( + // MARK: DistRunnerTests + name: "DistRunnerTests", + dependencies: [ + "DistRunner" + ] + ), + .target( + // MARK: DistWorker + name: "DistWorker", + dependencies: [ + "CurrentlyBeingProcessedBucketsTracker", + "EventBus", + "Extensions", + "Logging", + "Models", + "PluginManager", + "QueueClient", + "RESTMethods", + "ResourceLocationResolver", + "Scheduler", + "SimulatorPool", + "SynchronousWaiter", + "Timer", + "SPMUtility" + ] + ), + .testTarget( + // MARK: DistWorkerTests + name: "DistWorkerTests", + dependencies: [ + "DistWorker", + "ModelsTestHelpers", + "Scheduler" + ] + ), + .target( + // MARK: EventBus + name: "EventBus", + dependencies: [ + "Logging", + "Models" + ] + ), + .testTarget( + // MARK: EventBusTests + name: "EventBusTests", + dependencies: [ + "EventBus", + "ModelsTestHelpers", + "SynchronousWaiter" + ] + ), + .target( + // MARK: Extensions + name: "Extensions", + dependencies: [ + ] + ), + .testTarget( + // MARK: ExtensionsTests + name: "ExtensionsTests", + dependencies: [ + "Extensions", + "SPMUtility" + ] + ), + .target( + // MARK: FakeFbxctest + name: "FakeFbxctest", + dependencies: [ + "Extensions", + "TestingFakeFbxctest" + ] + ), + .target( + // MARK: fbxctest + name: "fbxctest", + dependencies: [ + "Ansi", + "JSONStream", + "LocalHostDeterminer", + "Logging", + "Metrics", + "Models", + "ProcessController", + "Timer", + "SPMUtility" + ] + ), + .target( + // MARK: FileCache + name: "FileCache", + dependencies: [ + "Extensions", + "SPMUtility" + ] + ), + .testTarget( + // MARK: FileCacheTests + name: "FileCacheTests", + dependencies: [ + "FileCache" + ] + ), + .target( + // MARK: FileHasher + name: "FileHasher", + dependencies: [ + "AtomicModels", + "Extensions", + "Models" + ] + ), + .testTarget( + // MARK: FileHasherTests + name: "FileHasherTests", + dependencies: [ + "FileHasher", + "TempFolder" + ] + ), + .target( + // MARK: LocalHostDeterminer + name: "LocalHostDeterminer", + dependencies: [ + "Logging" + ] + ), + .target( + // MARK: JSONStream + name: "JSONStream", + dependencies: [] + ), + .testTarget( + // MARK: JSONStreamTests + name: "JSONStreamTests", + dependencies: [ + "SPMUtility", + "JSONStream" + ] + ), + .target( + // MARK: JunitReporting + name: "JunitReporting", + dependencies: [ + ] + ), + .testTarget( + // MARK: JunitReportingTests + name: "JunitReportingTests", + dependencies: [ + "Extensions", + "JunitReporting" + ] + ), + .target( + // MARK: LaunchdUtils + name: "LaunchdUtils", + dependencies: [ + ] + ), + .testTarget( + // MARK: LaunchdUtilsTests + name: "LaunchdUtilsTests", + dependencies: [ + "LaunchdUtils" + ] + ), + .target( + // MARK: ListeningSemaphore + name: "ListeningSemaphore", + dependencies: [ + ] + ), + .testTarget( + // MARK: ListeningSemaphoreTests + name: "ListeningSemaphoreTests", + dependencies: [ + "ListeningSemaphore" + ] + ), + .target( + // MARK: LocalQueueServerRunner + name: "LocalQueueServerRunner", + dependencies: [ + "AutomaticTermination", + "DateProvider", + "Logging", + "Models", + "PortDeterminer", + "QueueServer", + "ScheduleStrategy", + "SynchronousWaiter", + "Version" + ] + ), + .testTarget( + // MARK: LocalQueueServerRunnerTests + name: "LocalQueueServerRunnerTests", + dependencies: [ + "AutomaticTermination", + "LocalQueueServerRunner" + ] + ), + .target( + // MARK: Logging + name: "Logging", + dependencies: [ + "Ansi", + "Extensions" + ] + ), + .target( + // MARK: LoggingSetup + name: "LoggingSetup", + dependencies: [ + "Ansi", + "GraphiteClient", + "IO", + "LocalHostDeterminer", + "Logging", + "Metrics", + "Sentry", + "SPMUtility", + "Version" + ] + ), + .testTarget( + // MARK: LoggingTests + name: "LoggingTests", + dependencies: [ + "Logging", + "SPMUtility" + ] + ), + .target( + // MARK: Metrics + name: "Metrics", + dependencies: [] + ), + .testTarget( + // MARK: MetricsTests + name: "MetricsTests", + dependencies: [ + "Metrics" + ] + ), + .target( + // MARK: Models + name: "Models", + dependencies: [ + "Extensions" + ] + ), + .target( + // MARK: ModelsTestHelpers + name: "ModelsTestHelpers", + dependencies: [ + "Models", + "ScheduleStrategy" + ] + ), + .testTarget( + // MARK: ModelsTests + name: "ModelsTests", + dependencies: [ + "Models", + "ModelsTestHelpers", + "TempFolder" + ] + ), + .target( + // MARK: Plugin + name: "Plugin", + dependencies: [ + "EventBus", + "JSONStream", + "Logging", + "LoggingSetup", + "Models", + "SimulatorVideoRecorder", + "Starscream", + "SynchronousWaiter", + "TestsWorkingDirectorySupport", + "SPMUtility" + ] + ), + .target( + // MARK: PluginManager + name: "PluginManager", + dependencies: [ + "EventBus", + "LocalHostDeterminer", + "Logging", + "ResourceLocationResolver", + "Models", + "ProcessController", + "Scheduler", + "Swifter", + "SynchronousWaiter" + ] + ), + .testTarget( + // MARK: PluginManagerTests + name: "PluginManagerTests", + dependencies: [ + "EventBus", + "Models", + "ModelsTestHelpers", + "PluginManager", + "ResourceLocationResolver", + "SPMUtility" + ] + ), + .target( + // MARK: PortDeterminer + name: "PortDeterminer", + dependencies: [ + "Logging", + "Swifter" + ] + ), + .testTarget( + // MARK: PortDeterminerTests + name: "PortDeterminerTests", + dependencies: [ + "PortDeterminer", + "Swifter" + ] + ), + .target( + // MARK: ProcessController + name: "ProcessController", + dependencies: [ + "Extensions", + "Logging", + "ResourceLocationResolver", + "Timer", + "SPMUtility" + ] + ), + .testTarget( + // MARK: ProcessControllerTests + name: "ProcessControllerTests", + dependencies: [ + "Extensions", + "ProcessController", + "SPMUtility" + ] + ), + .target( + // MARK: QueueClient + name: "QueueClient", + dependencies: [ + "Logging", + "Models", + "RESTMethods", + "SynchronousWaiter", + "Version", + "SPMUtility" + ] + ), + .testTarget( + // MARK: QueueClientTests + name: "QueueClientTests", + dependencies: [ + "Models", + "ModelsTestHelpers", + "PortDeterminer", + "QueueClient", + "QueueServer", + "RESTMethods", + "Swifter", + "SynchronousWaiter" + ] + ), + .target( + // MARK: QueueServer + name: "QueueServer", + dependencies: [ + "AutomaticTermination", + "BalancingBucketQueue", + "BucketQueue", + "DateProvider", + "EventBus", + "Extensions", + "FileHasher", + "Logging", + "Metrics", + "Models", + "PortDeterminer", + "RESTMethods", + "ResultsCollector", + "ScheduleStrategy", + "Swifter", + "SynchronousWaiter", + "Timer", + "Version", + "WorkerAlivenessTracker" + ] + ), + .testTarget( + // MARK: QueueServerTests + name: "QueueServerTests", + dependencies: [ + "AutomaticTermination", + "BalancingBucketQueue", + "BucketQueue", + "BucketQueueTestHelpers", + "DateProviderTestHelpers", + "Deployer", + "EventBus", + "FileHasher", + "Models", + "ModelsTestHelpers", + "QueueServer", + "ResourceLocationResolver", + "RESTMethods", + "ResultsCollector", + "ScheduleStrategy", + "TempFolder", + "VersionTestHelpers", + "WorkerAlivenessTracker", + "WorkerAlivenessTrackerTestHelpers" + ] + ), + .target( + // MARK: RemotePortDeterminer + name: "RemotePortDeterminer", + dependencies: [ + "QueueClient", + "Version" + ] + ), + .target( + // MARK: RemotePortDeterminerTestHelpers + name: "RemotePortDeterminerTestHelpers", + dependencies: [ + "RemotePortDeterminer" + ] + ), + .testTarget( + // MARK: RemotePortDeterminerTests + name: "RemotePortDeterminerTests", + dependencies: [ + "RemotePortDeterminer" + ] + ), + .target( + // MARK: RemoteQueue + name: "RemoteQueue", + dependencies: [ + "DistDeployer", + "Models", + "RemotePortDeterminer", + "SSHDeployer", + "Version" + ] + ), + .testTarget( + // MARK: RemoteQueueTests + name: "RemoteQueueTests", + dependencies: [ + "RemotePortDeterminerTestHelpers", + "RemoteQueue", + "VersionTestHelpers" + ] + ), + .target( + // MARK: ResultsCollector + name: "ResultsCollector", + dependencies: [ + "Models" + ] + ), + .testTarget( + // MARK: ResultsCollectorTests + name: "ResultsCollectorTests", + dependencies: [ + "ModelsTestHelpers", + "ResultsCollector" + ] + ), + .target( + // MARK: ResourceLocationResolver + name: "ResourceLocationResolver", + dependencies: [ + "AtomicModels", + "Extensions", + "FileCache", + "Models", + "URLResource" + ] + ), + .testTarget( + // MARK: ResourceLocationResolverTests + name: "ResourceLocationResolverTests", + dependencies: [ + "FileCache", + "ResourceLocationResolver", + "Swifter", + "TempFolder", + "URLResource" + ] + ), + .target( + // MARK: RESTMethods + name: "RESTMethods", + dependencies: [ + "Models", + "Version" + ] + ), + .target( + // MARK: Runner + name: "Runner", + dependencies: [ + "EventBus", + "fbxctest", + "LocalHostDeterminer", + "Logging", + "Models", + "SimulatorPool", + "TempFolder", + "TestsWorkingDirectorySupport" + ] + ), + .testTarget( + // MARK: RunnerTests + name: "RunnerTests", + dependencies: [ + "Extensions", + "Models", + "ModelsTestHelpers", + "ResourceLocationResolver", + "Runner", + "ScheduleStrategy", + "SimulatorPool", + "TestingFakeFbxctest", + "TempFolder" + ] + ), + .target( + // MARK: RuntimeDump + name: "RuntimeDump", + dependencies: [ + "EventBus", + "Extensions", + "Metrics", + "Models", + "Runner", + "SynchronousWaiter", + "TempFolder" + ] + ), + .testTarget( + // MARK: RuntimeDumpTests + name: "RuntimeDumpTests", + dependencies: [ + "Models", + "ModelsTestHelpers", + "ResourceLocationResolver", + "RuntimeDump", + "TestingFakeFbxctest", + "TempFolder" + ] + ), + .target( + // MARK: Sentry + name: "Sentry", + dependencies: [] + ), + .testTarget( + // MARK: SentryTests + name: "SentryTests", + dependencies: [ + "Sentry" + ] + ), + .target( + // MARK: Scheduler + name: "Scheduler", + dependencies: [ + "EventBus", + "ListeningSemaphore", + "Logging", + "Models", + "Runner", + "RuntimeDump", + "ScheduleStrategy", + "SimulatorPool", + "SynchronousWaiter", + "TempFolder" + ] + ), + .target( + // MARK: ScheduleStrategy + name: "ScheduleStrategy", + dependencies: [ + "Extensions", + "Logging", + "Models" + ] + ), + .testTarget( + // MARK: ScheduleStrategyTests + name: "ScheduleStrategyTests", + dependencies: [ + "Models", + "ModelsTestHelpers", + "ScheduleStrategy" + ] + ), + .target( + // MARK: SignalHandling + name: "SignalHandling", + dependencies: [ + "Models", + "Signals" + ] + ), + .testTarget( + // MARK: SignalHandlingTests + name: "SignalHandlingTests", + dependencies: [ + "SignalHandling", + "Signals" + ] + ), + .target( + // MARK: SimulatorPool + name: "SimulatorPool", + dependencies: [ + "Extensions", + "fbxctest", + "Logging", + "Models", + "ProcessController", + "TempFolder", + "SPMUtility" + ] + ), + .testTarget( + // MARK: SimulatorPoolTests + name: "SimulatorPoolTests", + dependencies: [ + "Models", + "ModelsTestHelpers", + "ResourceLocationResolver", + "SimulatorPool", + "SynchronousWaiter", + "TempFolder" + ] + ), + .target( + // MARK: SimulatorVideoRecorder + name: "SimulatorVideoRecorder", + dependencies: [ + "Logging", + "Models", + "ProcessController" + ] + ), + .target( + // MARK: SSHDeployer + name: "SSHDeployer", + dependencies: [ + "Ansi", + "Extensions", + "Logging", + "Models", + "SPMUtility", + "Deployer", + "Shout" + ] + ), + .testTarget( + // MARK: SSHDeployerTests + name: "SSHDeployerTests", + dependencies: [ + "SSHDeployer" + ] + ), + .target( + // MARK: SynchronousWaiter + name: "SynchronousWaiter", + dependencies: [] + ), + .testTarget( + // MARK: SynchronousWaiterTests + name: "SynchronousWaiterTests", + dependencies: ["SynchronousWaiter"] + ), + .target( + // MARK: TempFolder + name: "TempFolder", + dependencies: [ + "SPMUtility" + ] + ), + .testTarget( + // MARK: TempFolderTests + name: "TempFolderTests", + dependencies: [ + "TempFolder" + ] + ), + .target( + // MARK: TestingFakeFbxctest + name: "TestingFakeFbxctest", + dependencies: [ + "Extensions", + "fbxctest", + "Logging" + ] + ), + .target( + // MARK: TestingPlugin + name: "TestingPlugin", + dependencies: [ + "Extensions", + "Models", + "Logging", + "LoggingSetup", + "Plugin" + ] + ), + .target( + // MARK: TestsWorkingDirectorySupport + name: "TestsWorkingDirectorySupport", + dependencies: [ + "Models" + ] + ), + .target( + // MARK: Timer + name: "Timer", + dependencies: [ + ] + ), + .target( + // MARK: URLResource + name: "URLResource", + dependencies: [ + "FileCache", + "Logging", + "Models", + "SynchronousWaiter", + "SPMUtility" + ] + ), + .testTarget( + // MARK: URLResourceTests + name: "URLResourceTests", + dependencies: [ + "FileCache", + "Swifter", + "URLResource", + "SPMUtility" + ] + ), + .target( + // MARK: Version + name: "Version", + dependencies: [ + "FileHasher" + ] + ), + .target( + // MARK: VersionTestHelpers + name: "VersionTestHelpers", + dependencies: [ + "Version" + ] + ), + .testTarget( + // MARK: VersionTests + name: "VersionTests", + dependencies: [ + "Extensions", + "FileHasher", + "Version", + "SPMUtility" + ] + ), + .target( + // MARK: WorkerAlivenessTracker + name: "WorkerAlivenessTracker", + dependencies: [ + "Logging" + ] + ), + .target( + // MARK: WorkerAlivenessTrackerTestHelpersTests + name: "WorkerAlivenessTrackerTestHelpers", + dependencies: [ + "WorkerAlivenessTracker" + ] + ), + .testTarget( + // MARK: WorkerAlivenessTrackerTests + name: "WorkerAlivenessTrackerTests", + dependencies: [ + "WorkerAlivenessTracker", + "WorkerAlivenessTrackerTestHelpers" + ] + ), + .target( + // MARK: XcTestRun + name: "XcTestRun", + dependencies: [ + ] + ), + .testTarget( + // MARK: XcTestRunTests + name: "XcTestRunTests", + dependencies: [ + "XcTestRun" + ] + ) + ] +) diff --git a/test/manager/swift/index.spec.js b/test/manager/swift/index.spec.js new file mode 100644 index 0000000000..15ab7c4bb9 --- /dev/null +++ b/test/manager/swift/index.spec.js @@ -0,0 +1,203 @@ +const fs = require('fs'); +const path = require('path'); +const { extractPackageFile } = require('../../../lib/manager/swift/extract'); +const { updateDependency } = require('../../../lib/manager/swift/update'); + +const pkgContent = fs.readFileSync( + path.resolve(__dirname, `./_fixtures/SamplePackage.swift`), + 'utf8' +); + +describe('lib/manager/swift', () => { + describe('extractPackageFile()', () => { + it('returns null for empty content', () => { + expect(extractPackageFile(null)).toBeNull(); + expect(extractPackageFile(``)).toBeNull(); + expect(extractPackageFile(`dependencies:[]`)).toBeNull(); + expect(extractPackageFile(`dependencies:["foobar"]`)).toBeNull(); + }); + it('returns null for invalid content', () => { + expect(extractPackageFile(`dependen`)).toBeNull(); + expect(extractPackageFile(`dependencies!: `)).toBeNull(); + expect(extractPackageFile(`dependencies :`)).toBeNull(); + expect(extractPackageFile(`dependencies...`)).toBeNull(); + expect(extractPackageFile(`dependencies:!`)).toBeNull(); + expect(extractPackageFile(`dependencies:[`)).toBeNull(); + expect(extractPackageFile(`dependencies:[...`)).toBeNull(); + expect(extractPackageFile(`dependencies:[]`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package.package(`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(asdf`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package]`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(]`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(.package(`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(]`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(url],`)).toBeNull(); + expect( + extractPackageFile(`dependencies:[.package(url.package(]`) + ).toBeNull(); + expect( + extractPackageFile(`dependencies:[.package(url:.package(`) + ).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(url:]`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(url:"fo`)).toBeNull(); + expect(extractPackageFile(`dependencies:[.package(url:"fo]`)).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://example.com/something.git"]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git"]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git".package(]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", ]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", .package(]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", .exact(]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", from]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", from.package(` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", from:]` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git", from:.package(` + ) + ).toBeNull(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3")]` + ) + ).toBeNull(); + }); + it('parses package descriptions', () => { + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git",from:"1.2.3")]` + ) + ).toMatchSnapshot(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"...)]` + ) + ).toMatchSnapshot(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"..."1.2.4")]` + ) + ).toMatchSnapshot(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"..<"1.2.4")]` + ) + ).toMatchSnapshot(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git",..."1.2.3")]` + ) + ).toMatchSnapshot(); + expect( + extractPackageFile( + `dependencies:[.package(url:"https://github.com/vapor/vapor.git",..<"1.2.3")]` + ) + ).toMatchSnapshot(); + }); + it('parses multiple packages', () => { + expect(extractPackageFile(pkgContent)).toMatchSnapshot(); + }); + }); + describe('updateDependency()', () => { + it('updates successfully', () => { + [ + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]', + '1.2.4', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.4")]', + ], + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", from: "1.2.3")]', + '1.2.4', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", from: "1.2.4")]', + ], + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..."1.2.4")]', + '"1.2.3"..."1.2.5"', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..."1.2.5")]', + ], + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..<"1.2.4")]', + '"1.2.3"..<"1.2.5"', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..<"1.2.5")]', + ], + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..."1.2.4")]', + '..."1.2.5"', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..."1.2.5")]', + ], + [ + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..<"1.2.4")]', + '..<"1.2.5"', + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..<"1.2.5")]', + ], + ].forEach(([content, newValue, result]) => { + const { deps } = extractPackageFile(content); + const [dep] = deps; + const upgrade = { ...dep, newValue }; + const updated = updateDependency(content, upgrade); + expect(updated).toEqual(result); + }); + }); + it('returns content if already updated', () => { + const content = + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]'; + const currentValue = '1.2.3'; + const newValue = '1.2.4'; + const { deps } = extractPackageFile(content); + const [dep] = deps; + const upgrade = { ...dep, newValue }; + const replaced = content.replace(currentValue, newValue); + const updated = updateDependency(replaced, upgrade); + expect(updated).toBe(replaced); + }); + it('returns null if content is different', () => { + const content = + 'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]'; + const currentValue = '1.2.3'; + const newValue = '1.2.4'; + const { deps } = extractPackageFile(content); + const [dep] = deps; + const upgrade = { ...dep, newValue }; + const replaced = content.replace(currentValue, '1.2.5'); + expect(updateDependency(replaced, upgrade)).toBe(null); + }); + }); +}); diff --git a/test/versioning/swift.spec.js b/test/versioning/swift.spec.js new file mode 100644 index 0000000000..ad86d20090 --- /dev/null +++ b/test/versioning/swift.spec.js @@ -0,0 +1,85 @@ +const { + getNewValue, + isValid, + minSatisfyingVersion, + maxSatisfyingVersion, + isLessThanRange, + matches, +} = require('../../lib/versioning/swift'); + +describe('isValid(input)', () => { + it('understands Swift version ranges', () => { + expect(isValid('from: "1.2.3"')).toBe(true); + expect(isValid('from : "1.2.3"')).toBe(true); + expect(isValid('from:"1.2.3"')).toBe(true); + expect(isValid(' from:"1.2.3" ')).toBe(true); + expect(isValid(' from : "1.2.3" ')).toBe(true); + + expect(isValid('"1.2.3"..."1.2.4"')).toBe(true); + expect(isValid(' "1.2.3" ... "1.2.4" ')).toBe(true); + + expect(isValid('"1.2.3"...')).toBe(true); + expect(isValid(' "1.2.3" ... ')).toBe(true); + + expect(isValid('..."1.2.4"')).toBe(true); + expect(isValid(' ... "1.2.4" ')).toBe(true); + + expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true); + expect(isValid(' "1.2.3" ..< "1.2.4" ')).toBe(true); + + expect(isValid('..<"1.2.4"')).toBe(true); + expect(isValid(' ..< "1.2.4" ')).toBe(true); + }); + it('should return null for irregular versions', () => { + expect(isValid('17.04.0')).toBeFalsy(); + }); + it('should support simple semver', () => { + expect(isValid('1.2.3')).toBe(true); + }); + it('should support semver with dash', () => { + expect(isValid('1.2.3-foo')).toBe(true); + }); + it('should reject semver without dash', () => { + expect(isValid('1.2.3foo')).toBeFalsy(); + }); + it('should support ranges', () => { + expect(isValid('~1.2.3')).toBeFalsy(); + expect(isValid('^1.2.3')).toBeFalsy(); + expect(isValid('from: "1.2.3"')).toBe(true); + expect(isValid('"1.2.3"..."1.2.4"')).toBe(true); + expect(isValid('"1.2.3"..."1.2.4"')).toBe(true); + expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true); + expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true); + expect(isValid('..."1.2.3"')).toBe(true); + expect(isValid('..<"1.2.4"')).toBe(true); + expect( + minSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..<"1.2.4"') + ).toBe('1.2.3'); + expect( + maxSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..<"1.2.4"') + ).toBe('1.2.3'); + expect( + maxSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..."1.2.4"') + ).toBe('1.2.4'); + expect(isLessThanRange('1.2.3', '..."1.2.4"')).toBe(false); + expect(isLessThanRange('1.2.3', '"1.2.4"...')).toBe(true); + expect(matches('1.2.4', '..."1.2.4"')).toBe(true); + expect(matches('1.2.4', '..."1.2.3"')).toBe(false); + }); +}); +describe('getNewValue()', () => { + it('supports range update', () => { + [ + ['1.2.3', 'auto', '1.2.3', '1.2.4', '1.2.3'], + ['from: "1.2.3"', 'auto', '1.2.3', '1.2.4', '1.2.4'], + ['"1.2.3"...', 'auto', '1.2.3', '1.2.4', '"1.2.4"...'], + ['"1.2.3"..."1.2.4"', 'auto', '1.2.3', '1.2.5', '"1.2.3"..."1.2.5"'], + ['"1.2.3"..<"1.2.4"', 'auto', '1.2.3', '1.2.5', '"1.2.3"..<"1.2.5"'], + ['..."1.2.4"', 'auto', '1.2.3', '1.2.5', '..."1.2.5"'], + ['..<"1.2.4"', 'auto', '1.2.3', '1.2.5', '..<"1.2.5"'], + ].forEach(([range, strategy, fromVersion, toVersion, result]) => { + const newValue = getNewValue(range, strategy, fromVersion, toVersion); + expect(newValue).toEqual(result); + }); + }); +}); diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap index 8df23e4618..fd6c9711c1 100644 --- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap @@ -92,6 +92,9 @@ Object { "sbt": Array [ Object {}, ], + "swift": Array [ + Object {}, + ], "terraform": Array [ Object {}, ], diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index 19432cfb90..a1cbbe890b 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -1080,6 +1080,22 @@ Use this field to suppress various types of warnings and other notifications fro The above config will suppress the comment which is added to a PR whenever you close a PR unmerged. +## swift + +Anything other than `.exact(<...>)` will be treated as range with respect to Swift specific. +Because of this, some PR descriptions will look like `from: <...> => <...>`. + +Examples: + +```swift +package(name: "<...>", from: "1.2.3") // => from: "2.0.0" +package(name: "<...>", "1.2.3"...) // => "2.0.0"... +package(name: "<...>", "1.2.3"..."1.3.0") // => "1.2.3"..."2.0.0" +package(name: "<...>", "1.2.3"..<"1.3.0") // => "1.2.3"..<"2.0.0" +package(name: "<...>", ..."1.2.3") // => ..."2.0.0" +package(name: "<...>", ..<"1.2.3") // => ..<"2.0.0" +``` + ## terraform Currently Terraform support is limited to Terraform registry sources and github sources that include semver refs, e.g. like `github.com/hashicorp/example?ref=v1.0.0`.