| Age | Commit message (Collapse) | Author |
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Run upstream Composer and Shirabe over pristine copies of a fixture
project at the same path and require composer.lock, the whole vendor tree
(including the plugin-generated GeneratedConfig.php), the plugin's IO
lines and the exit code to match byte for byte.
The plugin under test (phpstan/extension-installer 1.4.3) is downloaded by
fixtures/e2e/fetch — pinned to an upstream commit and hash-verified — into
a git-ignored directory rather than committed; the test skips while it is
absent, like the other real-PHP prerequisites. Its dependencies are
minimal stand-ins resolved from local repositories, so test runs stay
offline. A zip dist would exercise the known lossy-string byte-precision
debt in RemoteFilesystem, so the fixture serves the plugin through a path
dist until that is resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The PHP worker is shared across the test binary, so whichever subscriber
test runs first owns the bare class name and every later install registers
the plugin under a _composer_tmpN rename (as upstream does). The exact-name
lookup made the removal test depend on lock acquisition order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wire removePlugin to EventDispatcher::removeListener now that subscriber
listeners carry the plugin's P-table handle: PHP's $candidate[0] ===
$listener identity check maps to phandle equality on Callable::PhpMethod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface
and Capable become fallible and dyn-compatible (their sole implementor is
the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners
register as Callable::PhpMethod and are invoked with a per-call event handle,
and the R table now drops entries when a child-side stub destructs. The R
table keeps its IndexMap with monotonically increasing handles, so released
handles are never reused and no generation counter is needed.
Upstream has no subscriber-plugin test, so the path is covered by a
Shirabe-owned fixture exercising all three getSubscribedEvents shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
InstallationManager::execute now takes &self (the mock recorder moved
into a RefCell) and its callers hold only shared borrows: plugin
registration inside a batch re-enters the same manager handle through
Composer::getInstallationManager()->getInstallPath(), which panicked
on the RefCell re-borrow under the &mut shape — the same re-entrancy
the repository side already fixed, unreachable from the ported tests
because they call PluginInstaller::install directly like PHPUnit does.
The worker-side InstalledVersions mirror now matches the full tail of
FilesystemRepository::write: unconditional reload plus the
reflection-based selfDir/installedIsLocalDir restore. The previous
class_exists(false) guard rested on a lazy-load assumption that does
not hold in the worker (its real ClassLoader only knows the Composer
checkout's vendor dir, so a later lazy load would read the checkout's
installed.php, not the project's); the mirror is now skipped only when
the class is not autoloadable at all, i.e. no plugin runtime and hence
no observer code. Boot-time seeding stays TODO(plugin).
Also from the review: registered_plugins entries are removed only
after the deactivate/uninstall loop (PHP unsets last, and a throw must
leave the entry observable); extra.class keeps associative-array
values and fails loudly on non-strings instead of silently dropping
them; the two discarded write() results now propagate (they carry the
reload-push failure); register_package's allow-plugins skip message is
DEBUG like the addPlugin side; the loader-eviction divergence of
REGISTERED_LOADERS and the lossy UTF-8 spots carry searchable
markers; the test-only proxy downcast follows the __ naming rule; the
R-table dispatch clones the entity out instead of holding the table
borrow across the handler; the IO/PartialComposer stubs turn a
plugin-side `new NullIO()` into an explicit error instead of an
ArgumentCountError; and the empty() emulation covers float 0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the remainder of PluginManager::registerPackage: the plugin
autoload map is built by the ported createLoader/parseAutoloads and
served to the worker over the existing reverse-RPC autoloader, files
entries go through a composerRequire-equivalent glue call, and
already-defined classes take the upstream _composer_tmp rename/eval
path. Instantiation uses the new NewObject/CallPhpMethod lanes backed
by a P table in the worker; PhpPluginProxy adapts the resulting handle
to PluginInterface, with $composer/$io exposed to plugin callbacks via
an R table (unsupported methods stay explicit errors). Hand-written
proxy stubs cover Composer, PartialComposer and the IO hierarchy, and
the stub autoloader is re-prepended after loading the Composer PHP
runtime so its vendor autoloader cannot shadow proxied FQCNs.
FilesystemRepository::write now mirrors InstalledVersions::reload into
a running worker (class_exists-guarded, so an unloaded class keeps its
upstream lazy-load behavior), removing the previously undefined
observation window.
The installer pipeline passes the installed repository as a shared
handle instead of a long-lived `&mut dyn`: plugin registration runs
inside InstallationManager::execute and re-enters the same local
repository through the RepositoryManager, which would panic on the
RefCell re-borrow under the old shape.
PluginInterface lifecycle methods now take an owned ComposerHandle
(plugins retain $composer past the call) and return anyhow::Result
(PHP plugin code may throw); the plugin list uses shared ownership so
the identity comparison of removePlugin survives the dual storage in
registeredPlugins, matching PHP reference semantics.
Ports the activate/upgrade/uninstall tests of PluginInstallerTest,
serialized across the shared worker process whose persistent class
table is exactly what exercises the rename path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the two script execution paths that previously stopped at
todo!(): a Class::method listener is invoked as CallStaticMethod with the
event crossing the boundary as a proxy-stub handle, and a Command-class
listener runs inside a throwaway bare Symfony Application hosted by the
worker via a generated snippet, its BufferedOutput written back through
the dispatcher's IO. makeAutoloader is ported for real (canonical-package
hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the
class_exists/is_callable/is_a/defined guards now query the worker, whose
script autoloader resolves classes by asking the Rust-side ClassLoader
over the reverse channel. EventInterface gains as_any (the IOInterface
downcast pattern) so the concrete event type is reachable behind the
trait object. Application::do_run now registers ScriptAliasCommand
entries as typed commands, unblocking the run-script --list/alias tests;
the dev-mode-to-generator test is ported with local mockall mocks. The
remaining ignored tests carry re-verified reasons: the listener methods
live on the PHPUnit test class itself (unloadable in the worker), or the
test needs live import of a user PHP Command class into the Application.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
test_preserve_indentation_after_read and test_overwrites_indentation_by_default
copy to and delete the same fixture path; PHPUnit runs them serially, the
parallel Rust harness let them race and flake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Three fixes for the ComposerRepository/FilesystemRepository/PlatformRepository
hazards where inner-composition delegation skipped PHP's late-bound virtual
dispatch:
- ComposerRepository::has_package now builds its packageMap through the
late-bound getPackages() equivalent, so lazy-providers repos surface the
LogicException and available-packages repos load their package list, as in
PHP, instead of silently answering false from the raw array.
- RepositoryInterface::get_repo_name returns anyhow::Result<String>: PHP's
getRepoName() counts through the late-bound initialize(), which is fallible
in file-reading subclasses. FilesystemRepository and PackageRepository now
run that initialization instead of freezing the inner array repository to an
empty state (which also made a later write() truncate installed.json).
Supporting changes keep the initialization chain callable from &self:
JsonFile::read takes &self (indent moved into a RefCell), FilesystemRepository
dev_mode became a Cell, and WritableArrayRepository dev_package_names a
RefCell.
- PlatformRepository::new routes constructor packages through its own
add_package so the override handling and full platform initialization run
as they do via PHP's parent constructor; the inner find_package/add_package
delegations inside add_package (and ComposerRepository::add_package) gained
the same is_initialized guard, since the constructor path would otherwise
freeze the repository.
Same defect class as 7db937af, 97b5211a and 3e367f78.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Add NO_COLOR=1, the COMPOSER/COMPOSER_VENDOR_DIR/COMPOSER_BIN_DIR clears and
the timezone normalization, and make bootstrap() Once-guarded so additional
call sites stay safe. Wiring it into every test binary still needs a libtest
setup hook (ctor crate or fixture-level calls), recorded in the TODO.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's JsonFile::encode throws a RuntimeException when json_encode fails; the
port swallowed that into an .unwrap() marked TODO(phase-c). Return
anyhow::Result from encode/encode_with_options and propagate at every call
site (print_table and list_repositories become Result-returning to carry it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Adds the Symfony console test helper (Tester/CommandCompletionTester) and
ports every CompletionFunctionalTest data-provider entry as an individual
test. The tests reproduce the PHP environment by chdir'ing into the
vendored composer/ checkout (its composer.json/lock provide the installed
packages, scripts and package properties the expectations reference); the
Packagist-backed entries query the live repository exactly like the PHP
test does. Only `exec ` is ignored: its expectations require the dev
checkout's fully installed vendor/bin, which the vendored checkout does
not ship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
IOInterface::ask/select now return anyhow::Result<PhpMixed>, and
ConsoleIO::ask_question forwards QuestionHelper errors (validator
failures, MissingInputException) instead of collapsing them with
.expect(). In PHP these exceptions propagate from QuestionHelper
through ConsoleIO to the caller, so callers such as UpdateCommand's
interactive package selection must be able to observe them; the
MissingInputException is wrapped with its concrete type preserved so
Application's ExceptionInterface downcast keeps working. All call
sites now propagate with `?` (Perforce::query_p4_user becomes
Result-returning: PHP declares it void but exceptions still escape),
and the previously ignored
test_interactive_mode_throws_if_no_package_entered passes.
ask_confirmation/ask_and_hide_answer still collapse errors; extending
propagation to them is left as TODO(phase-c) pending a decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's ArrayRepository::count()/hasPackage() call $this->initialize(),
which late-binds to the concrete repository class and lazily loads its
packages. The Rust pass-throughs skipped that: they ran ArrayRepository's
stub initialize instead, returning 0/false and marking the repository
initialized with an empty package list, which made ensure_initialized()
skip the real initialization forever after.
Take &mut self in RepositoryInterface::count/has_package so the lazy
repositories (Filesystem, Platform, Composer) can guard with their real
initialize, and return Result from has_package since that initialization
can fail (PHP propagates the exception). InstallerInterface::is_installed
and InstallationManager::is_package_installed/mark_alias_installed
propagate the same way, which also resolves the TODO(phase-d) markers on
Package/Path/Artifact/Vcs repositories about initialization errors being
swallowed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The only callers were trivial fixed-format uses: reading the first
four hash bytes as a native int, splitting in_addr byte strings, and
building a constant ZIP EOCD record. Each site now does the byte
manipulation directly, so the general-purpose shims are no longer
needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
A native binary never ships vendor/composer/installed.json, so
Composer's "non-standard Composer installation" warning fired on every
diagnose run and forced exit 1. The self-audit itself stays: a Composer
source snapshot is planned to be embedded together with the plugin API
implementation, which will make it functional; until then the missing
file reports success, marked with TODO(phase-c).
Also un-ignore diagnose_command_test::test_cmd_success: the other half
of its ignore reason ("requires real network access") is no blocker —
the PHP original runs its live packagist/github checks unguarded too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Glob::toRegex emits PCRE-only constructs — the (?=[^\.]) look-ahead for
the strict-leading-dot rule, the possessive [^/]++ in /**/ segments,
and, via BaseExcludeFilter, the (?=$|/) dir-boundary look-ahead — which
the regex crate cannot compile, so `archive` and every
ArchivableFilesFinder path panicked. Rewrite the port to tokenize the
glob (mirroring the PHP loop's dispatch) and resolve every no-dot
constraint by recursive union expansion. The dir boundary must take
part in that expansion (a trailing `*` matching zero characters drops
the constraint onto the boundary itself), so BaseExcludeFilter now uses
the new Glob::to_regex_dir_boundary instead of string surgery.
Equivalence was verified against PHP 8.5.8 (vendored Glob.php +
preg_match) over 66,176 glob x flag x subject combinations with zero
divergence. Un-ignores the five archiver tests blocked on this and
updates GitExcludeFilterTest's expected pattern text, an explicitly
authorized exception to the no-test-modification rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Resolve the remaining todo!()s in SymfonyStyle, OutputStyle,
QuestionHelper and SymfonyQuestionHelper:
* Wire up the virtual dispatch PHP performs for the protected
writePrompt()/writeError() overrides, following the codebase's
established inheritance idiom (Command, ArchiveDownloader): the base
class becomes a trait (QuestionHelperInterface, named after the
QuestionInterface precedent) whose provided methods ask/do_ask/
validate_attempts carry the template logic and late-bind the
write_prompt/write_error hooks through Self, with inner()/inner_mut()
reaching the base-class state. SymfonyQuestionHelper overrides the
hooks as plain trait-impl methods, mirroring PHP's protected-method
overriding, so SymfonyStyle-driven questions now render the Symfony
Style Guide prompt.
* Type definition_list input as an enum (string|array|TableSeparator)
because PhpMixed intentionally cannot carry objects; the
InvalidArgumentException branch (a LogicException) becomes
unrepresentable. horizontal_table now takes typed Cells/Rows.
* Propagate the MissingInputException thrown inside autocomplete()
through a Result instead of aborting.
* Implement as_console_output_interface via Ref::filter_map on
ConsoleOutput, the interface's only implementor.
* Port progressIterate eagerly, following ProgressBar::iterate.
* Map __FILE__ to current_exe(): a native binary never runs from a
phar, so the hiddeninput.exe relocation branch correctly never fires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Adopt the tar, flate2, and bzip2 crates to fill in the phar.rs and
compress.rs todos: PharData tar/zip reading, building, and whole-archive
compression, plus a native .phar reader that follows the php.net
file-format manual and verifies hash-based signatures. Callers now
propagate the constructor/extract errors PHP throws, and fwrite accepts
byte strings so gzread no longer needs lossy UTF-8.
The native .phar writing API stays todo!() (no call sites; Composer's
Compiler is not ported) and OPENSSL phar signatures are accepted
unverified (TODO(phase-c)).
This unblocks Tar::getComposerJson and the tar/phar/gzip downloaders;
tar_test (7), artifact_repository_test (2), and phar_archiver_test zip
(1) are un-ignored. The archive command itself still panics because
ArchiveManager::archive always generates glob excludes whose look-ahead
regexes the regex crate cannot compile; converting those patterns to
regex-compatible ones is a separate, still-undecided work item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
diagnose used to read hardcoded shim stubs, so it described a fictional
runtime: OPENSSL_VERSION_NUMBER was always 0 and tripped the TLSv1.1/1.2
check, PHP_BINARY and OPENSSL_VERSION_TEXT were empty, and the extension,
function and ini probes answered from a fixed table.
The PHP worker gained a `diagnose` entry that returns every fact the
command needs as one PHP array, cached in a OnceLock so the several call
sites share a single round trip. Reading it back needed array support in
the serialize() parser, which in turn lets get_loaded_extensions and
get_all_ini_files return real lists instead of comma-joined strings.
Also fixes the openssl_version message, which dropped strstr()'s
before_needle argument during the port, and check_connectivity's
allow_url_fopen test, which did not follow PHP string truthiness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The `fixed` option was read as a temporary inside the argument list of
update_requirements_after_resolution(), so the Ref lived until the end of the
enclosing let statement — i.e. across the whole call. When the resolved version
looks like a feature branch, that call asks for confirmation, and ConsoleIO
takes the same input RefCell mutably, panicking with "RefCell already borrowed".
Hoisting the read into its own statement ends the borrow before the call.
The expected output of the un-ignored test transcribed PHP's string
concatenation operator (`[y,n]? '.'`, used to keep the trailing space visible)
as a literal `.`; it now matches RequireCommandTest.php.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
findBestVersionForPackage is the only findBestCandidate() caller that PHP hands
$this->getIO() to; the port passed None, so VersionSelector silently skipped
every "Cannot use <pkg> as it requires <ext> which is missing from your
platform" warning. require/update/create-project therefore dropped a candidate
without telling the user why.
Un-ignores require_command_test::test_require, whose first data-provider case
asserts exactly that warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PHP's SecurityAdvisoryPoolFilter stores advisory *object references* in
$securityRemovedVersions, and PoolOptimizer::applyRemovalsToPool hands
that array to the new Pool by copy-on-write. Porting AnySecurityAdvisory
as a value type turned both of those into deep copies.
Measured on `require laravel/framework` (offline, warm cache): 113
distinct advisories were duplicated into 314,309 copies of ~1.08 KiB,
retaining 331.9 MiB in the filter loop and another 331.9 MiB when
apply_removals_to_pool cloned the whole map.
3.54s -> 2.62s (-26%), peak RSS 975 MB -> 343 MB, which matches the upper
bound measured by ablation. Composer runs the same workload in 1.49s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Operations are only ever constructed by the dependency resolver, so a
plugin has no way to inject an implementation of its own and the set is
closed. Modelling it as an enum, like AnyPackage, removes the
OperationInterface trait together with its two parallel downcast
mechanisms (as_any() + downcast_ref, and as_*_operation()) and the
get_package() default method that panicked on UpdateOperation.
The PHP idiom `$op instanceof UpdateOperation ? getTargetPackage() :
getPackage()`, written out at six call sites, becomes
AnyOperation::get_target_package(). InstallationManager's three blocks
that matched on the type string and then recovered the type with
expect() collapse into exhaustive matches.
SolverOperation keeps only its TYPE constant; the shared
getOperationType()/__toString() implementations move to AnyOperation,
which also drops the five Self::TYPE.to_string() allocations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
is_callable
RequireCommand registers an inline listener on InstallerEvents::PRE_OPERATIONS_EXEC
to track dependency_resolution_completed, mirroring PHP's `function () use
(&$dependencyResolutionCompleted) { ... }`. This is Composer's own code, not a
Plugin subscriber, but it went through the shared non-string-callable path, which
checked is_callable() against a hardcoded PhpMixed::Null and always failed,
breaking every `require` that reaches the install step.
Callable::Closure now carries the actual Rc<dyn Fn> instead of being a data-less
placeholder, and is invoked directly (Closures are always callable in PHP). The
ArrayCallable path used by future Plugin subscribers is untouched.
Un-ignoring the two require_command_test cases that cited this bug reveals two
separate, pre-existing issues (a missing ext-requirement warning message, and a
RefCell re-entrancy panic in ConsoleIO::ask_question); their #[ignore] reasons
are updated to describe the real current blocker instead of the now-fixed one.
|
|
4a5a9556 split each installer/installer-slow fixture into its own
#[test] fn (with #[ignore] on known failures) so they could be triaged
one at a time, and said explicitly that it should be reverted to a
single loop-based test per group once triage was done. Only
github-issues-7665.test still fails, and its cause is now understood
(see the previous commit): an upstream Composer defect, not a porting
bug. Restore the three loop-based tests, skipping that one fixture
inline with a TODO(phase-d) comment instead of a per-fixture #[ignore].
|
|
The ignore reason for slow_github_issues_7665 said "unknown reason,
needs further investigation." Investigation since then traced the
mismatch to an upstream Composer defect: Problem::getPrettyString's
RULE_LEARNED tie-break comparator (getSortableString() <=>
getSortableString()) is not transitive, and the values it compares are
literal ids that shift with the platform package count, which
Installer::createPlatformRepo() derives from the real ambient PHP
runtime and which Composer's own test suite never mocks. This fixture
is brittle to whichever extensions are installed on the machine
generating or running it. Nothing to fix on the Rust side; this is an
upstream Composer bug (composer/composer#12111 introduced the
comparator).
|
|
Problem::getPrettyString sorts same-priority reasons by
getSortableString(), whose RULE_LEARNED key is a '-'-joined literal id
string (e.g. "-95"). PHP's <=> compares two numeric strings
numerically, but the port used plain String::cmp (byte-wise), which
reverses relative order for same-length negative-number keys. Added
shirabe_php_shim::loosely_compare to approximate PHP's <=> for this
pattern (numeric compare when both sides parse as numbers, else byte
compare) and switched the sort comparator to use it.
The diagnosis this replaces (from the commit being amended) blamed
Pool package-id assignment order diverging from PHP under
COMPOSER_POOL_OPTIMIZER=0. That was disproven this session: direct
instrumentation of both PHP and the Rust port confirmed identical
relative package-id order, including on the ~335-package
github-issues-7665 fixture (ids matched up to a constant +2 offset
from a platform-mock package count difference). The sort comparator
was the actual bug, not package loading order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
create_extension_hint's --ignore-platform-req suggestion list sorted
missing extensions alphabetically before deduping. PHP's array_unique
removes duplicates while preserving first-occurrence order instead,
which for this call site matches the order problems were reported in
(root-require-not-found problems before SAT-conflict problems).
Replaced the sort+dedup with the existing order-preserving
shirabe_php_shim::array_unique, which was already ported for exactly
this PHP semantic but wasn't used here.
|
|
PHPUnit's expectException/expectExceptionMessage wrap the whole rest
of InstallerTest::doTestIntegration, so a fixture's expected exception
can legitimately come from FactoryMock::create() itself (root package
construction), not just the later install/update run. The Rust port
only checked for that at one point, after the run, and unconditionally
unwrapped Factory::__create_mock's result — so a fixture like
install-self-from-root.test (root package requiring itself, which
throws during root package construction) panicked on that unwrap
instead of being caught and compared against the expected message.
Capture the construction Result and, when an exception was expected,
run the same normalize/contains/assert check the later block already
uses before returning early, matching PHPUnit's behavior of running no
further test-method code once the expected exception has fired.
|
|
ArrayDumper::dump built the mirrors field as PhpMixed::Array keyed by
stringified index ("0", "1", ...), which this codebase's PhpMixed
JSON serialization renders as an object. PHP just assigns the plain,
sequentially-keyed mirrors array directly, which json_encode renders
as a JSON array. Use PhpMixed::List instead, matching the actual
shape written to composer.lock.
|
|
Package::set_source_dist_references and LockTransaction's dist-url
mirroring both used {(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i, but the regex
crate has no look-around support at all and panics compiling it. Per
docs/dev/regex-porting.md, rewrote the boundary assertions into
capturing groups and switched to Preg::replace_callback, which
re-emits the captured delimiters around the replaced reference.
|
|
For a simple, non-lazy composer repository (no providers-url/
metadata-url/providers-lazy-url), load_packages() delegates to
self.inner.load_packages() (PHP: parent::loadPackages()). PHP's
ArrayRepository::loadPackages() calls $this->getPackages(), which
virtual-dispatches through ComposerRepository::getPackages() down to
ComposerRepository::initialize() (the real HTTP/file fetch).
Composition doesn't get that dispatch for free: self.inner is a bare
ArrayRepository, so self.inner.load_packages() ends up calling
ArrayRepository::initialize() — a no-op stub that just sets an empty
package list — instead of ComposerRepository::initialize(). The
repository's packages.json fetched and parsed successfully, but the
result was silently discarded, so the repo always looked empty.
get_packages() already carries the identical guard with the same
diagnosis in its own comment; load_packages() was just missing it.
Documented the same latent gap in find_package/find_packages/count/
has_package, which call into ArrayRepository the same unguarded way
but aren't known to be exercised by any test yet.
|
|
PHP's InstallerTest::setUp() does chdir(__DIR__) before every test so
relative "type": "path" repository URLs in fixtures resolve against
composer/tests/Composer/Test; tearDown() restores the previous cwd.
This was never ported, only the env-var clears were, so any fixture
declaring a relative path repo failed at PathRepository::initialize()
with "the `url` supplied for the path (...) repository does not
exist" — not a missing PathRepository feature, just an unset cwd.
Turned TearDown into a real guard that captures and chdirs on
construction and restores on Drop. Since set_current_dir is
process-wide state and tests run concurrently by default, added
#[serial] to test_installer (the only call site not already inside a
#[serial] macro-generated fn) so no two TearDown-holding tests can
race on cwd; serial_test's plain #[serial] shares one unnamed lock
across the whole binary, so this is sufficient.
|
|
PoolBuilder::build_pool() and warn_about_non_matching_update_allow_list()
called get_canonical_packages() on the locked repository during a
partial update, but PHP's PoolBuilder uses the plain getPackages().
get_canonical_packages() unwraps AliasPackage down to its base package,
discarding the alias's own version identity.
Locker::get_locked_repository() wraps a lock entry with
extra.branch-alias in a single CompleteAliasPackage rather than adding
a separate base object, so canonicalizing it silently dropped the
locked branch-alias version (e.g. 2.2.x-dev), leaving only the raw
dev-master version behind. For a package excluded from the partial
update's allow-list, that meant its locked branch-alias version could
no longer satisfy the root's constraint, producing a spurious solver
conflict instead of keeping the package pinned exactly as locked.
Fixed the same bug in AuditCommand's --locked package listing, which
had the identical get_canonical_packages()/getPackages() mismatch
against AuditCommand.php.
|
|
Two shim gaps made the extension-related branches of solver-problem
messages wrong:
- phpversion($ext) with a non-empty extension can't be known statically
(it's shirabe-php-shim's todo!()); Problem::get_missing_package_reason
now calls shirabe_php_rpc::phpversion, the same RPC bridge
platform::runtime::Runtime::get_extension_version already uses.
- extension_loaded's hardcoded allowlist was missing "pcre", a mandatory
always-compiled-in PHP extension, so ext-pcre was misreported as
"missing from your system" instead of "disabled by your platform
config" whenever a platform override disabled it.
Also fix XdebugHandler::getAllIniFiles() always returning `[""]`
(a php-runtime stub): create_extension_hint()'s early-return guard
(`paths[0] empty && len==1`) fired unconditionally, silently dropping
the entire "To enable extensions..." hint from every solver-problem
message that mentions missing extensions. shirabe-external-packages
can't depend on shirabe-php-rpc (shirabe-php-rpc already depends on
shirabe-external-packages), so IniHelper::get_all() queries a new
get_all_ini_files RPC command directly instead of going through the
stub.
This exposed that XdebugHandler is never constructed with a name
because bin/composer's restart-without-Xdebug bootstrap was never
ported to main.rs, making COMPOSER_ORIGINAL_INIS-driven behavior
unreachable; documented with a TODO(phase-c) and updated
ini_helper_test.rs's ignore reasons (and ignored test_with_no_ini,
which only passed before by coincidence with the old stub's constant
output) to match.
|
|
PHP's readTestFile splits sections with a regex that leaves a section's
own trailing newline attached when it is the file's last section
(verified against real PHP); for install-without-lock.test and
update-without-lock.test, --EXPECT-LOCK-- is that last section, so its
raw content is "false\n", not "false". PHP's `$expectLock === 'false'`
check therefore also misses, but PHP falls through to
JsonFile::parseJson("false\n"), which json_decodes to boolean false
anyway, and every downstream check in doTestIntegration branches on
PHP truthiness of $expectLock, so the outcome is unaffected either way.
The Rust port only mirrored the literal string comparison, so the
same "false\n" produced ExpectLock::Json(Value::Bool(false)) instead
of ExpectLock::Never, and do_test_integration's Json arm unconditionally
read composer.lock, panicking because config.lock=false means no lock
file is ever written. Parse EXPECT-LOCK as JSON first and classify by
PHP truthiness of the result, matching what doTestIntegration actually
checks instead of the raw string.
|
|
The b'>' match arm in strip_tags's state machine never pushed the
character to the output buffer when encountered outside a tag (state
0), unlike every other special-character arm (!, ?, -, and the
catch-all) which does push in that state. Every literal '>' not part
of an HTML-like tag was silently dropped.
This corrupted "=>" into "=" in Composer's operation trace strings
(e.g. "Upgrading foo/bar (1.0.0 => 1.1.0)"), which go through
strip_tags to remove the <info>/<comment> markup before comparison,
un-ignoring 82 integration tests that were asserting on that exact
arrow.
|
|
Locker::get_stability_flags returned IndexMap<String, String>, converting
each value via PhpMixed::as_string(), which only matches the String
variant. composer.lock's "stability-flags" values are always JSON
integers (BasePackage::STABILITIES), so every flag silently decoded to
"" and installer.rs's downstream .parse::<i64>() defaulted it to 0
(stable). This made any locked package pinned via a non-stable
stability-flags entry look "unacceptable" during `composer install`,
silently dropping it from the solver's pool instead of fixing/requiring
it — turning a real dependency conflict into a spurious "lock file needs
changes" result. Return i64 directly, matching
RootPackageInterface::get_stability_flags and set_lock_data's existing
convention for this same PHP array shape.
|
|
do_test_integration built the Locker's composer.json content string with
serde_json::to_string, which doesn't escape slashes, while the fixture's
auto-computed lock "hash" field used JsonFile::encode_with_options with
slashes escaped (mirroring PHP's json_encode default). Since virtually
every package name contains a "/", this made Locker::isFresh() spuriously
report every such lock as stale, unignoring 12 fixtures that depended on
the lock being recognized as fresh during `install`.
|
|
Replace the three loop-based test functions (each iterating all
fixtures with a shared SKIP_INSTALLER_* skip list) with macro-generated
per-fixture test functions, so a single failing or panicking fixture
no longer hides the pass/fail status of the rest. Known-failing
fixtures keep their TODO(phase-d) reasons via #[ignore] on their own
test instead of a shared skip list. All generated tests are #[serial]
since they mutate global env vars (COMPOSER_POOL_OPTIMIZER,
COMPOSER_FUND) that would otherwise race across parallel test threads.
This is an interim structure for triaging the ~131 ignored fixtures
one at a time; it should be reverted to a single loop-based test per
group (matching Composer's directory-scanned fixtures) once they are
resolved.
|
|
JsonFile::write_with_options() used the caller-supplied JsonEncodeOptions
verbatim, ignoring self.indent (set by read()'s detect_indenting), unlike
PHP's write() which always passes $this->indent to encode() regardless of
the $options argument. Un-ignore test_preserve_indentation_after_read.
|
|
test_pool_builder
build_pool never populated skipped_load for locked packages skipped due
to the update allow list (nor their replace targets), so load_package's
skipped_load-driven transitive-unlock branch never fired.
load_packages_marked_for_loading never recorded loaded packages into
loaded_per_repo, so repositories had no way to dedupe already-loaded
versions when a constraint got re-expanded, producing duplicate pool
entries.
unlock_package looked up a locked package's removal index via the
position in a Vec snapshot of self.packages.values() instead of its
actual IndexMap key, so after earlier removals the wrong entry (or
none) got removed, leaving stale locked packages in the pool.
Fixing all three lets test_pool_builder run un-ignored.
|
|
shirabe_php_shim::chr() returned a Rust String, which lossily
re-encodes bytes >= 0x80 as UTF-8 replacement characters. ip_get_mask,
ip_get_network, and ip_map_to_6 relied on chr() to build raw in_addr
and netmask byte arrays, corrupting IPv4-in-IPv6 mappings and CIDR
netmasks. Build the Vec<u8> byte arrays directly instead of
round-tripping through String, and un-ignore test_ip_address and
test_ip_range now that the underlying bug is fixed.
chr()'s only other caller (http_downloader.rs, an ASCII ESC byte in a
regex pattern) didn't need the indirection either, so remove the shim
function entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
The ignored tests raced on the process-wide HTTP_PROXY/etc env vars
and the ProxyManager::INSTANCE mutex, causing spurious PoisonError
panics when run in parallel. Adding #[serial_test::serial], the same
annotation test_instantiation already used, fixes the race with no
other changes needed.
|
|
Several call sites coerced PhpMixed to bool via `.as_bool()` (which
only matches a literal Bool variant) where the corresponding PHP code
does a plain `(bool)` cast or truthy check (isset()/array_key_exists()
+ implicit bool conversion). This silently dropped truthy non-bool
values (e.g. String("true"), String("1"), Int(1)) to their unwrap_or
default instead of PHP's actual truthy result. Switched these sites to
PhpMixed::to_bool(), which implements PHP's full truthy-cast rules.
|
|
74 tests carried a bare #[ignore] with no explanation. Re-ran each:
25 now pass and had the attribute removed; the remaining 49 got a
concise reason (todo!() stubs, regex-crate PCRE gaps, PhpMixed type
mismatches, config bool-coercion bugs, missing skipped_load wiring
in PoolBuilder, etc.) so future work can find and fix them by grep.
No production code or test logic/assertions were changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
parseIgnoreWithApply distinguishes ['CVE-123' => 'reason'] from
[0 => 'CVE-123'] by checking is_int($key) in PHP. AuditConfig only
matched on the value's type, so a canonical-int-keyed string value
was mistaken for an id => reason pair. Expose canonical_int_key from
the php-shim to replicate PHP's key-canonicalization rule and
unignore test_mixed_formats.
|