1
0
Fork 0

Merge remote-tracking branch 'upstream/main' into git-merge-composer.lock-detection

pull/11517/head
Dan Wallis 2023-07-21 15:33:46 +01:00
commit c2ebc9532f
No known key found for this signature in database
GPG Key ID: A02198884F2740BC
49 changed files with 435 additions and 75 deletions

View File

@ -30,6 +30,7 @@ jobs:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
dependencies: [locked]
os: [ubuntu-latest]
experimental: [false]
@ -57,11 +58,11 @@ jobs:
- php-version: "8.2"
dependencies: lowest-ignore
os: ubuntu-latest
experimental: true
experimental: false
- php-version: "8.2"
dependencies: highest-ignore
os: ubuntu-latest
experimental: true
experimental: false
steps:
- name: "Checkout"

View File

@ -37,7 +37,7 @@
"symfony/filesystem": "^5.4 || ^6.0 || ^7",
"symfony/finder": "^5.4 || ^6.0 || ^7",
"symfony/process": "^5.4 || ^6.0 || ^7",
"react/promise": "^2.8",
"react/promise": "^2.8 || ^3",
"composer/pcre": "^2.1 || ^3.1",
"symfony/polyfill-php73": "^1.24",
"symfony/polyfill-php80": "^1.24",

29
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c50c89580fa044b7523cb55c2d557c87",
"content-hash": "4bceaf933dcf6bc05808134e78d21496",
"packages": [
{
"name": "composer/ca-bundle",
@ -692,23 +692,24 @@
},
{
"name": "react/promise",
"version": "v2.10.0",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38"
"reference": "c86753c76fd3be465d93b308f18d189f01a22be4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38",
"reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38",
"url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4",
"reference": "c86753c76fd3be465d93b308f18d189f01a22be4",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36"
"phpstan/phpstan": "1.10.20 || 1.4.10",
"phpunit/phpunit": "^9.5 || ^7.5"
},
"type": "library",
"autoload": {
@ -752,7 +753,7 @@
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
"source": "https://github.com/reactphp/promise/tree/v2.10.0"
"source": "https://github.com/reactphp/promise/tree/v3.0.0"
},
"funding": [
{
@ -760,7 +761,7 @@
"type": "open_collective"
}
],
"time": "2023-05-02T15:15:43+00:00"
"time": "2023-07-11T16:12:49+00:00"
},
{
"name": "seld/jsonlint",
@ -2034,16 +2035,16 @@
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.10.25",
"version": "1.10.26",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "578f4e70d117f9a90699324c555922800ac38d8c"
"reference": "5d660cbb7e1b89253a47147ae44044f49832351f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578f4e70d117f9a90699324c555922800ac38d8c",
"reference": "578f4e70d117f9a90699324c555922800ac38d8c",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f",
"reference": "5d660cbb7e1b89253a47147ae44044f49832351f",
"shasum": ""
},
"require": {
@ -2092,7 +2093,7 @@
"type": "tidelift"
}
],
"time": "2023-07-06T12:11:37+00:00"
"time": "2023-07-19T12:44:37+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",

View File

@ -968,7 +968,7 @@ performance.
* **--ignore-platform-req:** ignore a specific platform requirement (`php`, `hhvm`,
`lib-*` and `ext-*`) and skip the [platform check](07-runtime.md#platform-check) for it.
Multiple requirements can be ignored via wildcard.
* **--strict-psr:** Return a failed status code (1) if PSR-4 or PSR-0 mapping errors
* **--strict-psr:** Return a failed exit code (1) if PSR-4 or PSR-0 mapping errors
are present. Requires --optimize to work.
## clear-cache / clearcache / cc

View File

@ -101,6 +101,24 @@ optionally be an object with package name patterns for keys for more granular in
> configuration in global and package configurations the string notation
> is translated to a `*` package pattern.
## audit
Security audit configuration options
### ignored
A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.
```json
{
"config": {
"audit": {
"ignored": ["CVE-1234", "GHSA-xx", "PKSA-yy"]
}
}
}
```
## use-parent-dir
When running Composer in a directory where there is no composer.json, if there

View File

@ -1665,6 +1665,11 @@ parameters:
count: 3
path: ../src/Composer/Downloader/FileDownloader.php
-
message: "#^Strict comparison using \\=\\=\\= between null and Composer\\\\Util\\\\Http\\\\Response will always evaluate to false\\.$#"
count: 1
path: ../src/Composer/Downloader/FileDownloader.php
-
message: "#^Parameter \\#3 \\$cwd of method Composer\\\\Util\\\\ProcessExecutor\\:\\:execute\\(\\) expects string\\|null, string\\|false given\\.$#"
count: 5
@ -2305,11 +2310,6 @@ parameters:
count: 1
path: ../src/Composer/Installer/InstallationManager.php
-
message: "#^Only booleans are allowed in an if condition, React\\\\Promise\\\\PromiseInterface\\|null given\\.$#"
count: 1
path: ../src/Composer/Installer/InstallationManager.php
-
message: "#^Only booleans are allowed in an if condition, Symfony\\\\Component\\\\Console\\\\Helper\\\\ProgressBar\\|null given\\.$#"
count: 1

View File

@ -15,6 +15,7 @@ parameters:
excludePaths:
- '../tests/Composer/Test/Fixtures/*'
- '../tests/Composer/Test/Autoload/Fixtures/*'
- '../tests/Composer/Test/Autoload/MinimumVersionSupport/vendor/'
- '../tests/Composer/Test/Plugin/Fixtures/*'
- '../tests/Composer/Test/PolyfillTestCase.php'

View File

@ -325,6 +325,19 @@
"type": ["string"]
}
},
"audit": {
"type": "object",
"description": "Security audit configuration options",
"properties": {
"ignored": {
"type": "array",
"description": "A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.",
"items": {
"type": "string"
}
}
}
},
"notify-on-install": {
"type": "boolean",
"description": "Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour, defaults to true."

View File

@ -44,12 +44,18 @@ class Auditor
* @param PackageInterface[] $packages
* @param self::FORMAT_* $format The format that will be used to output audit results.
* @param bool $warningOnly If true, outputs a warning. If false, outputs an error.
* @param string[] $ignoredIds Ignored advisory IDs, remote IDs or CVE IDs
* @return int Amount of packages with vulnerabilities found
* @throws InvalidArgumentException If no packages are passed in
*/
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true): int
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoredIds = []): int
{
$advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
if (\count($ignoredIds) > 0) {
$advisories = $this->filterIgnoredAdvisories($advisories, $ignoredIds);
}
if (self::FORMAT_JSON === $format) {
$io->write(JsonFile::encode(['advisories' => $advisories]));
@ -73,6 +79,40 @@ class Auditor
return 0;
}
/**
* @phpstan-param array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> $advisories
* @param array<string> $ignoredIds
* @phpstan-return array<string, array<PartialSecurityAdvisory|SecurityAdvisory>>
*/
private function filterIgnoredAdvisories(array $advisories, array $ignoredIds): array
{
foreach ($advisories as $package => $pkgAdvisories) {
$advisories[$package] = array_filter($pkgAdvisories, static function (PartialSecurityAdvisory $advisory) use ($ignoredIds) {
if (in_array($advisory->advisoryId, $ignoredIds, true)) {
return false;
}
if ($advisory instanceof SecurityAdvisory) {
if (in_array($advisory->cve, $ignoredIds, true)) {
return false;
}
foreach ($advisory->sources as $source) {
if (in_array($source['remoteId'], $ignoredIds, true)) {
return false;
}
}
}
return true;
});
if (\count($advisories[$package]) === 0) {
unset($advisories[$package]);
}
}
return $advisories;
}
/**
* @param array<string, array<PartialSecurityAdvisory>> $advisories
* @return array{int, int} Count of affected packages and total count of advisories

View File

@ -63,7 +63,7 @@ EOT
$repoSet->addRepository($repo);
}
return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false));
return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $composer->getConfig()->get('audit')['ignored'] ?? []));
}
/**

View File

@ -556,8 +556,27 @@ EOT
return $vals;
},
],
'audit.ignore' => [
static function ($vals) {
if (!is_array($vals)) {
return 'array expected';
}
return true;
},
static function ($vals) {
return $vals;
},
],
];
// allow unsetting audit config entirely
if ($input->getOption('unset') && $settingKey === 'audit') {
$this->configSource->removeConfigSetting($settingKey);
return 0;
}
if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) {
if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) {
$this->getIO()->writeError('<info>You are now running Composer with SSL/TLS protection enabled.</info>');

View File

@ -442,6 +442,10 @@ EOT
$exitCode = 0;
$viewData = [];
$viewMetaData = [];
$writeVersion = false;
$writeDescription = false;
foreach (['platform' => true, 'locked' => true, 'available' => false, 'installed' => true] as $type => $showVersion) {
if (isset($packages[$type])) {
ksort($packages[$type]);
@ -616,14 +620,14 @@ EOT
$io->writeError('');
$io->writeError('<info>Direct dependencies required in composer.json:</>');
if (\count($directDeps) > 0) {
$this->printPackages($io, $directDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
} else {
$io->writeError('Everything up to date');
}
$io->writeError('');
$io->writeError('<info>Transitive dependencies not required in composer.json:</>');
if (\count($transitiveDeps) > 0) {
$this->printPackages($io, $transitiveDeps, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
} else {
$io->writeError('Everything up to date');
}
@ -631,7 +635,7 @@ EOT
if ($writeLatest && \count($packages) === 0) {
$io->writeError('All your direct dependencies are up to date');
} else {
$this->printPackages($io, $packages, $indent, $versionFits, $latestFits, $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
}
}
@ -649,15 +653,18 @@ EOT
*/
private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength): void
{
$padName = $writeVersion || $writeLatest || $writeDescription;
$padVersion = $writeLatest || $writeDescription;
$padLatest = $writeDescription;
foreach ($packages as $package) {
$link = $package['source'] ?? $package['homepage'] ?? '';
if ($link !== '') {
$io->write($indent . '<href='.OutputFormatter::escape($link).'>'.$package['name'].'</>'. str_repeat(' ', $nameLength - strlen($package['name'])), false);
$io->write($indent . '<href='.OutputFormatter::escape($link).'>'.$package['name'].'</>'. str_repeat(' ', ($padName ? $nameLength - strlen($package['name']) : 0)), false);
} else {
$io->write($indent . str_pad($package['name'], $nameLength, ' '), false);
$io->write($indent . str_pad($package['name'], ($padName ? $nameLength : 0), ' '), false);
}
if (isset($package['version']) && $writeVersion) {
$io->write(' ' . str_pad($package['version'], $versionLength, ' '), false);
$io->write(' ' . str_pad($package['version'], ($padVersion ? $versionLength : 0), ' '), false);
}
if (isset($package['latest']) && isset($package['latest-status']) && $writeLatest) {
$latestVersion = $package['latest'];
@ -666,7 +673,7 @@ EOT
if (!$io->isDecorated()) {
$latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion;
}
$io->write(' <' . $style . '>' . str_pad($latestVersion, $latestLength, ' ') . '</' . $style . '>', false);
$io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '</' . $style . '>', false);
}
if (isset($package['description']) && $writeDescription) {
$description = strtok($package['description'], "\r\n");
@ -815,7 +822,7 @@ EOT
$io->write('<info>homepage</info> : ' . $package->getHomepage());
$io->write('<info>source</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference()));
$io->write('<info>dist</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference()));
if ($installedRepo->hasPackage($package)) {
if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) {
$path = $this->requireComposer()->getInstallationManager()->getInstallPath($package);
if (is_string($path)) {
$io->write('<info>path</info> : ' . realpath($path));
@ -976,7 +983,7 @@ EOT
];
}
if ($installedRepo->hasPackage($package)) {
if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) {
$path = $this->requireComposer()->getInstallationManager()->getInstallPath($package);
if (is_string($path)) {
$path = realpath($path);

View File

@ -37,6 +37,7 @@ class Config
'allow-plugins' => [],
'use-parent-dir' => 'prompt',
'preferred-install' => 'dist',
'audit' => ['ignored' => []],
'notify-on-install' => true,
'github-protocols' => ['https', 'ssh', 'git'],
'gitlab-protocol' => null,
@ -207,6 +208,11 @@ class Config
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
}
} elseif ('audit' === $key) {
$currentIgnores = $this->config['audit']['ignored'];
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
$this->config['audit']['ignored'] = array_merge($currentIgnores, $val['ignored']);
} else {
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);

View File

@ -216,6 +216,7 @@ abstract class ArchiveDownloader extends FileDownloader
*
* @param string $file Extracted file
* @param string $path Directory
* @phpstan-return PromiseInterface<void|null>
*
* @throws \UnexpectedValueException If can not extract downloaded file to path
*/

View File

@ -174,6 +174,7 @@ class DownloadManager
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
* @phpstan-return PromiseInterface<void|null>
*
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
@ -241,6 +242,7 @@ class DownloadManager
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
* @phpstan-return PromiseInterface<void|null>
*/
public function prepare(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface
{
@ -258,6 +260,7 @@ class DownloadManager
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @phpstan-return PromiseInterface<void|null>
*
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
@ -279,6 +282,7 @@ class DownloadManager
* @param PackageInterface $initial initial package version
* @param PackageInterface $target target package version
* @param string $targetDir target dir
* @phpstan-return PromiseInterface<void|null>
*
* @throws \InvalidArgumentException if initial package is not installed
*/
@ -328,6 +332,7 @@ class DownloadManager
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @phpstan-return PromiseInterface<void|null>
*/
public function remove(PackageInterface $package, string $targetDir): PromiseInterface
{
@ -347,6 +352,7 @@ class DownloadManager
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param PackageInterface|null $prevPackage previous package instance in case of updates
* @phpstan-return PromiseInterface<void|null>
*/
public function cleanup(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface
{

View File

@ -34,6 +34,7 @@ interface DownloaderInterface
* This should do any network-related tasks to prepare for an upcoming install/update
*
* @param string $path download path
* @phpstan-return PromiseInterface<void|null|string>
*/
public function download(PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface;
@ -49,6 +50,7 @@ interface DownloaderInterface
* @param PackageInterface $package package instance
* @param string $path download path
* @param PackageInterface $prevPackage previous package instance in case of an update
* @phpstan-return PromiseInterface<void|null>
*/
public function prepare(string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface;
@ -57,6 +59,7 @@ interface DownloaderInterface
*
* @param PackageInterface $package package instance
* @param string $path download path
* @phpstan-return PromiseInterface<void|null>
*/
public function install(PackageInterface $package, string $path): PromiseInterface;
@ -66,6 +69,7 @@ interface DownloaderInterface
* @param PackageInterface $initial initial package
* @param PackageInterface $target updated package
* @param string $path download path
* @phpstan-return PromiseInterface<void|null>
*/
public function update(PackageInterface $initial, PackageInterface $target, string $path): PromiseInterface;
@ -74,6 +78,7 @@ interface DownloaderInterface
*
* @param PackageInterface $package package instance
* @param string $path download path
* @phpstan-return PromiseInterface<void|null>
*/
public function remove(PackageInterface $package, string $path): PromiseInterface;
@ -88,6 +93,7 @@ interface DownloaderInterface
* @param PackageInterface $package package instance
* @param string $path download path
* @param PackageInterface $prevPackage previous package instance in case of an update
* @phpstan-return PromiseInterface<void|null>
*/
public function cleanup(string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null): PromiseInterface;
}

View File

@ -536,6 +536,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
}
/**
* @phpstan-return PromiseInterface<void|null>
* @throws \RuntimeException
*/
protected function discardChanges(string $path): PromiseInterface
@ -551,6 +552,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
}
/**
* @phpstan-return PromiseInterface<void|null>
* @throws \RuntimeException
*/
protected function stashChanges(string $path): PromiseInterface

View File

@ -230,6 +230,9 @@ class SvnDownloader extends VcsDownloader
return "Could not retrieve changes between $fromReference and $toReference due to missing revision information";
}
/**
* @phpstan-return PromiseInterface<void|null>
*/
protected function discardChanges(string $path): PromiseInterface
{
if (0 !== $this->process->execute('svn revert -R .', $output, $path)) {

View File

@ -259,6 +259,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* if false (remove) the changes should be assumed to be lost if the operation is not aborted
*
* @throws \RuntimeException in case the operation must be aborted
* @phpstan-return PromiseInterface<void|null>
*/
protected function cleanChanges(PackageInterface $package, string $path, bool $update): PromiseInterface
{
@ -286,6 +287,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param string $path download path
* @param string $url package url
* @param PackageInterface|null $prevPackage previous package (in case of an update)
* @phpstan-return PromiseInterface<void|null>
*/
abstract protected function doDownload(PackageInterface $package, string $path, string $url, ?PackageInterface $prevPackage = null): PromiseInterface;
@ -295,6 +297,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param PackageInterface $package package instance
* @param string $path download path
* @param string $url package url
* @phpstan-return PromiseInterface<void|null>
*/
abstract protected function doInstall(PackageInterface $package, string $path, string $url): PromiseInterface;
@ -305,6 +308,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param PackageInterface $target updated package
* @param string $path download path
* @param string $url package url
* @phpstan-return PromiseInterface<void|null>
*/
abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, string $path, string $url): PromiseInterface;

View File

@ -105,6 +105,7 @@ class ZipDownloader extends ArchiveDownloader
*
* @param string $file File to extract
* @param string $path Path where to extract file
* @phpstan-return PromiseInterface<void|null>
*/
private function extractWithSystemUnzip(PackageInterface $package, string $file, string $path): PromiseInterface
{
@ -194,6 +195,7 @@ class ZipDownloader extends ArchiveDownloader
*
* @param string $file File to extract
* @param string $path Path where to extract file
* @phpstan-return PromiseInterface<void|null>
*/
private function extractWithZipArchive(PackageInterface $package, string $file, string $path): PromiseInterface
{

View File

@ -402,7 +402,7 @@ class Installer
$repoSet->addRepository($repo);
}
return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat) > 0 ? self::ERROR_AUDIT_FAILED : 0;
return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat, true, $this->config->get('audit')['ignored'] ?? []) > 0 ? self::ERROR_AUDIT_FAILED : 0;
} catch (TransportException $e) {
$this->io->error('Failed to audit '.$target.' packages.');
if ($this->io->isVerbose()) {

View File

@ -401,7 +401,7 @@ if [ -n "\$bashSource" ]; then
fi
fi
"\${dir}/$binFile" "\$@"
sh "\${dir}/$binFile" "\$@"
PROXY;
}

View File

@ -180,7 +180,7 @@ class InstallationManager
*/
public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false): void
{
/** @var array<callable(): ?PromiseInterface> */
/** @var array<callable(): ?PromiseInterface<void|null>> */
$cleanupPromises = [];
$signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) {
@ -237,8 +237,8 @@ class InstallationManager
/**
* @param OperationInterface[] $operations List of operations to execute in this batch
* @param array<callable(): ?PromiseInterface> $cleanupPromises
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, bool $downloadOnly, array $allOperations): void
{
@ -275,7 +275,7 @@ class InstallationManager
if ($opType !== 'uninstall') {
$promise = $installer->download($package, $initialPackage);
if ($promise) {
if (null !== $promise) {
$promises[] = $promise;
}
}
@ -322,8 +322,8 @@ class InstallationManager
/**
* @param OperationInterface[] $operations List of operations to execute in this batch
* @param array<callable(): ?PromiseInterface> $cleanupPromises
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function executeBatch(InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, bool $devMode, bool $runScripts, array $allOperations): void
{
@ -413,7 +413,7 @@ class InstallationManager
}
/**
* @param PromiseInterface[] $promises
* @param array<PromiseInterface<void|null>> $promises
*/
private function waitOnPromises(array $promises): void
{
@ -440,7 +440,7 @@ class InstallationManager
/**
* Executes download operation.
*
* $param PackageInterface $package
* @phpstan-return PromiseInterface<void|null>|null
*/
public function download(PackageInterface $package): ?PromiseInterface
{
@ -455,6 +455,7 @@ class InstallationManager
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param InstallOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function install(InstalledRepositoryInterface $repo, InstallOperation $operation): ?PromiseInterface
{
@ -471,6 +472,7 @@ class InstallationManager
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param UpdateOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function update(InstalledRepositoryInterface $repo, UpdateOperation $operation): ?PromiseInterface
{
@ -509,6 +511,7 @@ class InstallationManager
*
* @param InstalledRepositoryInterface $repo repository in which to check
* @param UninstallOperation $operation operation instance
* @phpstan-return PromiseInterface<void|null>|null
*/
public function uninstall(InstalledRepositoryInterface $repo, UninstallOperation $operation): ?PromiseInterface
{
@ -638,8 +641,8 @@ class InstallationManager
}
/**
* @param array<callable(): ?PromiseInterface> $cleanupPromises
* @return void
* @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
*/
private function runCleanup(array $cleanupPromises): void
{
@ -648,7 +651,7 @@ class InstallationManager
$this->loop->abortJobs();
foreach ($cleanupPromises as $cleanup) {
$promises[] = new \React\Promise\Promise(static function ($resolve, $reject) use ($cleanup): void {
$promises[] = new \React\Promise\Promise(static function ($resolve) use ($cleanup): void {
$promise = $cleanup();
if (!$promise instanceof PromiseInterface) {
$resolve();

View File

@ -48,6 +48,7 @@ interface InstallerInterface
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function download(PackageInterface $package, ?PackageInterface $prevPackage = null);
@ -63,6 +64,7 @@ interface InstallerInterface
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function prepare(string $type, PackageInterface $package, ?PackageInterface $prevPackage = null);
@ -72,6 +74,7 @@ interface InstallerInterface
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package);
@ -83,6 +86,7 @@ interface InstallerInterface
* @param PackageInterface $target updated version
* @throws InvalidArgumentException if $initial package is not installed
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target);
@ -92,6 +96,7 @@ interface InstallerInterface
* @param InstalledRepositoryInterface $repo repository in which to check
* @param PackageInterface $package package instance
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package);
@ -106,6 +111,7 @@ interface InstallerInterface
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
public function cleanup(string $type, PackageInterface $package, ?PackageInterface $prevPackage = null);

View File

@ -272,6 +272,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
/**
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
protected function installCode(PackageInterface $package)
{
@ -282,6 +283,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
/**
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
protected function updateCode(PackageInterface $initial, PackageInterface $target)
{
@ -316,6 +318,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
/**
* @return PromiseInterface|null
* @phpstan-return PromiseInterface<void|null>|null
*/
protected function removeCode(PackageInterface $package)
{

View File

@ -322,9 +322,8 @@ class VersionGuesser
$prettyVersion = 'dev-' . $candidateVersion;
if ($length === 0) {
foreach ($promises as $promise) {
if ($promise instanceof CancellablePromiseInterface) {
$promise->cancel();
}
// to support react/promise 2.x we wrap the promise in a resolve() call for safety
\React\Promise\resolve($promise)->cancel();
}
}
}

View File

@ -1070,6 +1070,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
return ['namesFound' => $namesFound, 'packages' => $packages];
}
/**
* @phpstan-return PromiseInterface<array{mixed, string}>
*/
private function startCachedAsyncDownload(string $fileName, ?string $packageName = null): PromiseInterface
{
if (null === $this->lazyProvidersUrl) {
@ -1598,6 +1601,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
}
}
/**
* @phpstan-return PromiseInterface<array<mixed>|true> true if the response was a 304 and the cache is fresh, otherwise it returns the decoded json
*/
private function asyncFetchFile(string $filename, string $cacheKey, ?string $lastModifiedTime = null): PromiseInterface
{
if ('' === $filename) {
@ -1610,7 +1616,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) {
// make it look like we got a 304 response
return \React\Promise\resolve(true);
/** @var PromiseInterface<true> $promise */
$promise = \React\Promise\resolve(true);
return $promise;
}
$httpDownloader = $this->httpDownloader;

View File

@ -238,7 +238,12 @@ class PlatformRepository extends ArrayRepository
$parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips);
$this->addLibrary($name.'-openssl'.($isFips ? '-fips' : ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', [], $isFips ? ['curl-openssl'] : []);
} else {
$this->addLibrary($name.'-'.$library, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']);
if ($library === '(securetransport) openssl') {
$shortlib = 'securetransport';
} else {
$shortlib = $library;
}
$this->addLibrary($name.'-'.$shortlib, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']);
}
}

View File

@ -310,7 +310,7 @@ class GitHubDriver extends VcsDriver
$resource = $this->getContents($resource['git_url'])->decodeJson();
}
if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) {
if (!isset($resource['content']) || $resource['encoding'] !== 'base64' || false === ($content = base64_decode($resource['content']))) {
throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
}

View File

@ -133,6 +133,7 @@ class Filesystem
*
* @throws \RuntimeException
* @return PromiseInterface
* @phpstan-return PromiseInterface<bool>
*/
public function removeDirectoryAsync(string $directory)
{
@ -784,6 +785,12 @@ class Filesystem
if (!is_dir($target)) {
throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 0, null, $target);
}
// Removing any previously junction to ensure clean execution.
if (!is_dir($junction) || $this->isJunction($junction)) {
@rmdir($junction);
}
$cmd = sprintf(
'mklink /J %s %s',
ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)),

View File

@ -357,6 +357,13 @@ class CurlDownloader
continue;
}
// TODO: Remove this as soon as https://github.com/curl/curl/issues/10591 is resolved
if ($errno === 55 /* CURLE_SEND_ERROR */) {
$this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG);
$this->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1]);
continue;
}
if ($errno === 28 /* CURLE_OPERATION_TIMEDOUT */ && PHP_VERSION_ID >= 70300 && $progress['namelookup_time'] === 0.0 && !$timeoutWarning) {
$timeoutWarning = true;
$this->io->writeError('<warning>A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.</warning>');

View File

@ -28,7 +28,7 @@ use React\Promise\PromiseInterface;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
* @phpstan-type Request array{url: non-empty-string, options: mixed[], copyTo: string|null}
* @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: TransportException}
* @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: \Throwable}
*/
class HttpDownloader
{
@ -123,6 +123,7 @@ class HttpDownloader
* although not all options are supported when using the default curl downloader
* @throws TransportException
* @return PromiseInterface
* @phpstan-return PromiseInterface<Http\Response>
*/
public function add(string $url, array $options = [])
{
@ -164,6 +165,7 @@ class HttpDownloader
* although not all options are supported when using the default curl downloader
* @throws TransportException
* @return PromiseInterface
* @phpstan-return PromiseInterface<Http\Response>
*/
public function addCopy(string $url, string $to, array $options = [])
{
@ -199,6 +201,7 @@ class HttpDownloader
/**
* @phpstan-param Request $request
* @return array{Job, PromiseInterface}
* @phpstan-return array{Job, PromiseInterface<Http\Response>}
*/
private function addJob(array $request, bool $sync = false): array
{

View File

@ -25,7 +25,7 @@ class Loop
private $httpDownloader;
/** @var ProcessExecutor|null */
private $processExecutor;
/** @var PromiseInterface[][] */
/** @var array<int, array<PromiseInterface<mixed>>> */
private $currentPromises = [];
/** @var int */
private $waitIndex = 0;
@ -52,18 +52,17 @@ class Loop
}
/**
* @param PromiseInterface[] $promises
* @param ?ProgressBar $progress
* @param array<PromiseInterface<mixed>> $promises
* @param ProgressBar|null $progress
*/
public function wait(array $promises, ?ProgressBar $progress = null): void
{
/** @var \Exception|null */
$uncaught = null;
\React\Promise\all($promises)->then(
static function (): void {
},
static function ($e) use (&$uncaught): void {
static function (\Throwable $e) use (&$uncaught): void {
$uncaught = $e;
}
);
@ -107,7 +106,7 @@ class Loop
}
unset($this->currentPromises[$waitIndex]);
if ($uncaught) {
if (null !== $uncaught) {
throw $uncaught;
}
}
@ -116,9 +115,8 @@ class Loop
{
foreach ($this->currentPromises as $promiseGroup) {
foreach ($promiseGroup as $promise) {
if ($promise instanceof CancellablePromiseInterface) {
$promise->cancel();
}
// to support react/promise 2.x we wrap the promise in a resolve() call for safety
\React\Promise\resolve($promise)->cancel();
}
}
}

View File

@ -155,6 +155,7 @@ class ProcessExecutor
*
* @param string|list<string> $command the command to execute
* @param string $cwd the working directory
* @phpstan-return PromiseInterface<Process>
*/
public function executeAsync($command, ?string $cwd = null): PromiseInterface
{

View File

@ -58,6 +58,7 @@ class SyncHelper
* Waits for a promise to resolve
*
* @param Loop $loop Loop instance which you can get from $composer->getLoop()
* @phpstan-param PromiseInterface<mixed>|null $promise
*/
public static function await(Loop $loop, ?PromiseInterface $promise = null): void
{

View File

@ -14,6 +14,7 @@ namespace Composer\Test\Advisory;
use Composer\Advisory\PartialSecurityAdvisory;
use Composer\Advisory\SecurityAdvisory;
use Composer\IO\BufferIO;
use Composer\IO\NullIO;
use Composer\Package\Package;
use Composer\Package\Version\VersionParser;
@ -71,6 +72,35 @@ class AuditorTest extends TestCase
$this->assertSame($expected, $result, $message);
}
public function testAuditIgnoredIDs(): void
{
$packages = [
new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
new Package('vendor1/package2', '3.0.0.0', '3.0.0'),
new Package('vendorx/packagex', '3.0.0.0', '3.0.0'),
new Package('vendor3/package1', '3.0.0.0', '3.0.0'),
];
$ignoredIds = ['CVE1', 'ID2', 'RemoteIDx'];
$auditor = new Auditor();
$result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, $ignoredIds);
$io->expects([
['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
['text' => 'Package: vendor3/package1'],
['text' => 'CVE: CVE5'],
['text' => 'Title: advisory7'],
['text' => 'URL: https://advisory.example.com/advisory7'],
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
], true);
$this->assertSame(1, $result);
// without ignored IDs, we should get all 4
$result = $auditor->audit($io, $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false);
$this->assertSame(4, $result);
}
private function getRepoSet(): RepositorySet
{
$repo = $this
@ -160,7 +190,7 @@ class AuditorTest extends TestCase
'sources' => [
[
'name' => 'source2',
'remoteId' => 'RemoteID2',
'remoteId' => 'RemoteID4',
],
],
'reportedAt' => '2022-05-25 13:21:00',
@ -205,14 +235,14 @@ class AuditorTest extends TestCase
[
'advisoryId' => 'IDx',
'packageName' => 'vendorx/packagex',
'title' => 'advisory7',
'link' => 'https://advisory.example.com/advisory7',
'title' => 'advisory17',
'link' => 'https://advisory.example.com/advisory17',
'cve' => 'CVE5',
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
'sources' => [
[
'name' => 'source2',
'remoteId' => 'RemoteID4',
'remoteId' => 'RemoteIDx',
],
],
'reportedAt' => '2015-05-25 13:21:00',

View File

@ -1,2 +1,2 @@
/vendor/
/composer.lock
/vendor/

View File

@ -57,7 +57,7 @@ class HomeCommandTest extends TestCase
$this->assertSame(trim($expected), trim($appTester->getDisplay(true)));
}
public function useCaseProvider(): Generator
public static function useCaseProvider(): Generator
{
yield 'Invalid or missing repository URL' => [
[

View File

@ -288,12 +288,19 @@ vendor/package 1.1.0 <highlight>! 1.2.0</highlight>", trim($appTester->getDispla
unlink('./composer.json');
unlink('./auth.json');
// listing packages
$appTester = $this->getApplicationTester();
$appTester->run(['command' => 'show', '-p' => true]);
$output = trim($appTester->getDisplay(true));
foreach (Regex::matchAll('{^(\w+)}m', $output)->matches as $m) {
self::assertTrue(PlatformRepository::isPlatformPackage((string) $m[1]));
}
// getting a single package
$appTester->run(['command' => 'show', '-p' => true, 'package' => 'php']);
$appTester->assertCommandIsSuccessful();
$appTester->run(['command' => 'show', '-p' => true, '-f' => 'json', 'package' => 'php']);
$appTester->assertCommandIsSuccessful();
}
public function testOutdatedWithZeroMajor(): void
@ -397,4 +404,45 @@ available:
installed:
vendor/installed 2.0.0 description of installed package', $output);
}
public function testNameOnlyPrintsNoTrailingWhitespace(): void
{
$this->initTempComposer([
'repositories' => [
'packages' => [
'type' => 'package',
'package' => [
// CAUTION: package names matter - output is sorted, and we want shorter before longer ones
['name' => 'vendor/apackage', 'description' => 'generic description', 'version' => '1.0.0'],
['name' => 'vendor/apackage', 'description' => 'generic description', 'version' => '1.1.0'],
['name' => 'vendor/longpackagename', 'description' => 'generic description', 'version' => '1.0.0'],
['name' => 'vendor/longpackagename', 'description' => 'generic description', 'version' => '1.1.0'],
['name' => 'vendor/somepackage', 'description' => 'generic description', 'version' => '1.0.0'],
],
],
],
]);
$this->createInstalledJson([
self::getPackage('vendor/apackage', '1.0.0'),
self::getPackage('vendor/longpackagename', '1.0.0'),
self::getPackage('vendor/somepackage', '1.0.0'),
]);
$appTester = $this->getApplicationTester();
$appTester->run(['command' => 'show', '-N' => true]);
self::assertSame(
'vendor/apackage
vendor/longpackagename
vendor/somepackage', trim($appTester->getDisplay(true))); // trim() is fine here, but see CAUTION above
$appTester = $this->getApplicationTester();
$appTester->run(['command' => 'show', '--outdated' => true, '-N' => true]);
self::assertSame(
'Legend:
! patch or minor release available - update recommended
~ major release available - update possible
vendor/apackage
vendor/longpackagename', trim($appTester->getDisplay(true))); // trim() is fine here, but see CAUTION above
}
}

View File

@ -129,7 +129,7 @@ class SuggestsCommandTest extends TestCase
self::assertSame(trim($expected), trim($appTester->getDisplay(true)));
}
public function provideSuggest(): \Generator
public static function provideSuggest(): \Generator
{
yield 'with lockfile, show suggested' => [
true,

View File

@ -314,7 +314,7 @@ class ZipDownloaderTest extends TestCase
}
/**
* @param ?\React\Promise\PromiseInterface $promise
* @param ?\React\Promise\PromiseInterface<mixed> $promise
*/
private function wait($promise): void
{
@ -329,7 +329,7 @@ class ZipDownloaderTest extends TestCase
$e = $ex;
});
if ($e) {
if ($e !== null) {
throw $e;
}
}

View File

@ -21,7 +21,7 @@ use Composer\Test\Mock\FactoryMock;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
class ArchiveManagerTest extends ArchiverTest
class ArchiveManagerTest extends ArchiverTestCase
{
/**
* @var ArchiveManager

View File

@ -17,7 +17,7 @@ use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor;
use Composer\Package\CompletePackage;
abstract class ArchiverTest extends TestCase
abstract class ArchiverTestCase extends TestCase
{
/**
* @var \Composer\Util\Filesystem

View File

@ -15,7 +15,7 @@ namespace Composer\Test\Package\Archiver;
use Composer\Package\Archiver\PharArchiver;
use Composer\Util\Platform;
class PharArchiverTest extends ArchiverTest
class PharArchiverTest extends ArchiverTestCase
{
public function testTarArchive(): void
{

View File

@ -16,7 +16,7 @@ use Composer\Util\Platform;
use ZipArchive;
use Composer\Package\Archiver\ZipArchiver;
class ZipArchiverTest extends ArchiverTest
class ZipArchiverTest extends ArchiverTestCase
{
/**
* @dataProvider provideGitignoreExcludeNegationTestCases

View File

@ -369,7 +369,6 @@ libSSH Version => libssh2/1.4.1',
'curl: libssh not libssh2' => [
'curl',
'
curl
cURL support => enabled
@ -412,6 +411,57 @@ libSSH Version => libssh/0.9.3/openssl/zlib',
],
[['curl_version', [], ['version' => '7.68.0']]],
],
'curl: SecureTransport' => [
'curl',
'
curl
cURL support => enabled
cURL Information => 8.1.2
Age => 10
Features
AsynchDNS => Yes
CharConv => No
Debug => No
GSS-Negotiate => No
IDN => Yes
IPv6 => Yes
krb4 => No
Largefile => Yes
libz => Yes
NTLM => Yes
NTLMWB => Yes
SPNEGO => Yes
SSL => Yes
SSPI => No
TLS-SRP => Yes
HTTP2 => Yes
GSSAPI => Yes
KERBEROS5 => Yes
UNIX_SOCKETS => Yes
PSL => No
HTTPS_PROXY => Yes
MULTI_SSL => Yes
BROTLI => Yes
ALTSVC => Yes
HTTP3 => No
UNICODE => No
ZSTD => Yes
HSTS => Yes
GSASL => No
Protocols => dict, file, ftp, ftps, gopher, gophers, http, https, imap, imaps, ldap, ldaps, mqtt, pop3, pop3s, rtmp, rtmpe, rtmps, rtmpt, rtmpte, rtmpts, rtsp, scp, sftp, smb, smbs, smtp, smtps, telnet, tftp
Host => aarch64-apple-darwin22.4.0
SSL Version => (SecureTransport) OpenSSL/3.1.1
ZLib Version => 1.2.11
libSSH Version => libssh2/1.11.0',
[
'lib-curl' => '8.1.2',
'lib-curl-securetransport' => ['3.1.1', ['lib-curl-openssl']],
'lib-curl-zlib' => '1.2.11',
'lib-curl-libssh2' => '1.11.0',
],
[['curl_version', [], ['version' => '8.1.2']]],
],
'date' => [
'date',
'

View File

@ -401,6 +401,34 @@ class GitHubDriverTest extends TestCase
];
}
public function testGetEmptyFileContent(): void
{
$repoUrl = 'http://github.com/composer/packagist';
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io->expects($this->any())
->method('isInteractive')
->will($this->returnValue(true));
$httpDownloader = $this->getHttpDownloaderMock($io, $this->config);
$httpDownloader->expects(
[
['url' => 'https://api.github.com/repos/composer/packagist', 'body' => '{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}'],
['url' => 'https://api.github.com/repos/composer/packagist/contents/composer.json?ref=main', 'body' => '{"encoding":"base64","content":""}'],
],
true
);
$repoConfig = [
'url' => $repoUrl,
];
$gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $this->getProcessExecutorMock());
$gitHubDriver->initialize();
$this->assertSame('', $gitHubDriver->getFileContent('composer.json', 'main'));
}
/**
* @param string|object $object
* @param mixed $value

View File

@ -12,6 +12,7 @@
namespace Composer\Test\Util;
use Composer\Util\Platform;
use Composer\Util\Filesystem;
use Composer\Test\TestCase;
@ -317,6 +318,38 @@ class FilesystemTest extends TestCase
$this->assertDirectoryDoesNotExist($junction, $junction . ' is not a directory');
}
public function testOverrideJunctions(): void
{
if (!Platform::isWindows()) {
$this->markTestSkipped('Only runs on windows');
}
@mkdir($this->workingDir.'/real/nesting/testing', 0777, true);
$fs = new Filesystem();
$old_target = $this->workingDir.'/real/nesting/testing';
$target = $this->workingDir.'/real/../real/nesting';
$junction = $this->workingDir.'/junction';
// Override non-broken junction
$fs->junction($old_target, $junction);
$fs->junction($target, $junction);
$this->assertTrue($fs->isJunction($junction), $junction.': is a junction');
$this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction');
//Remove junction
$this->assertTrue($fs->removeJunction($junction), $junction . ' has been removed');
// Override broken junction
$fs->junction($old_target, $junction);
$fs->removeDirectory($old_target);
$fs->junction($target, $junction);
$this->assertTrue($fs->isJunction($junction), $junction.': is a junction');
$this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction');
}
public function testCopy(): void
{
@mkdir($this->workingDir . '/foo/bar', 0777, true);

View File

@ -120,7 +120,6 @@ class ProcessExecutorTest extends TestCase
$process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG));
$process->enableAsync();
$start = microtime(true);
/** @var Promise $promise */
$promise = $process->executeAsync('sleep 2');
$this->assertEquals(1, $process->countActiveJobs());
$promise->cancel();