Conversation
Extends the coverage gate from the Android-specific layer to every class in the library, Java and Kotlin: - coverageReport<Variant> / coverageVerification<Variant> / verifyCoverage replace the androidLayer* tasks and cover ch.qos.logback.** and org.slf4j.impl.**, with the same rule: 100% of each class's lines and branches. - scripts/coverage-gaps.py lists the lines and branches a JaCoCo report misses. CI writes both variants' reports first and prints their gaps to the log and the job summary, then runs the gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
The phase-start checklist lists what CI runs; CI now also runs verifyCoverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Owner
Author
|
Build (JDK 17) and Build (JDK 21) fail at the new Coverage gate step, which is expected at this stage. Every earlier step passes: assemble, bytecode check, lint and both flavors' unit tests. This PR adds the gate, and the tests that satisfy it haven't landed yet. The Coverage gaps step lists what is uncovered:
The tests are being written package by package and will be pushed to this branch. CI should go green once they cover every line and branch. Generated by Claude Code |
…O, ThrowableProxyVO) Add LoggerContextVOTest, LoggingEventVOTest and ThrowableProxyVOTest. They pin what LoggingEventVO.build copies from its source (caller data only when present), the lazily computed and cached formatted message, the context accessors and getMdc, ThrowableProxyVO.build converting the whole cause chain and every suppressed proxy, toString of LoggerContextVO, and every null/non-null branch of equals/hashCode of the three classes (including the exact-class check of the event and throwable VOs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…als in spi VO tests Kill mutations of LoggingEventVO and LoggerContextVO that the previous tests let survive: build() computing caller data the source has not computed yet (LBCLASSIC-145), build() dropping the throwable proxy, equals() without its identity short-circuit, and the LoggerContext constructor not taking an exact copy of the property map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
OptionHelper.getEnv() and getSystemProperties() caught a SecurityException from System.getenv(String) and System.getProperties(). Those only throw it when a SecurityManager denies access, and none can be in place where the library runs: Android never has one (System.setSecurityManager throws there). The unit tests can't install one either: JDK 18+ refuses to at runtime unless started with -Djava.security.manager=allow, and CI gates coverage on JDK 21 too. Unlike System.getProperty and System.setProperty, which delegate to the installed Properties object (and so can be made to throw by a test), these two calls give a test no way in, so the handlers could never be covered. Both methods now return the JDK call's result directly; their result is unchanged for every input reachable on Android. Tests pin that getEnv() reads the environment and getSystemProperties() returns the system properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Brings the 20 core.util classes CI reported short of full line/branch coverage to 100%. The tests pin: - COWArrayList: every List operation, and that each modification refreshes the typed array copy. - CharSequenceToRegexMapper: rejected backslash and repeated quotes, literal characters with repeat counts. - CloseUtil: null arguments are ignored and close failures of closeables, sockets and server sockets are suppressed. - ContentTypeUtil, ContextUtil, EnvUtil, FixedDelay, PropertySetterException, LocationUtil, StringCollectionUtil: remaining overloads, null/invalid inputs and their exact results or exception messages. - Duration and FileSize: unparsable input, toString units, unbounded duration, and the IllegalStateException for a unit their pattern admits but valueOf() doesn't handle (a copy of the class initialized with a wider pattern via Mockito's static mock of Pattern.compile). - ExecutorServiceUtil: pool configuration, daemon "logback-N" threads, already-daemon threads left untouched, and shutdown interrupting tasks. - FileUtil: prefixRelativePath branches, the default AndroidContextUtil fallback, and copy() failures (reported as an error status and RolloverFailure; both streams closed even when closing fails). - InterruptUtil: masking/unmasking, and an error status when re-interrupting the current thread is denied. - Loader: resource lookup and counting, TCL/Class.forName fallbacks, and, through copies of Loader re-initialized by FreshCopyClassLoader, the logback.ignoreTCL property and the getClassLoader permission check. - OptionHelper: instantiation errors (null name, incompatible class, loading failures), ScanException wrapping, SecurityException handling of system property reads/writes (injected through a denying Properties object), the Android property fallback, toBoolean, isEmpty and extractDefaultReplacement. - StatusListenerConfigHelper: installIfAsked for plain, context-aware and life-cycle listeners, listeners rejected by the status manager, and listener classes that can't be instantiated. - StatusPrinter: null contexts, contexts without a status manager, error/ warning thresholds, status lists, and throwable line formatting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
The getClassLoader permission tests re-run Loader's static initializer in a restricted protection domain, which relies on AccessController checking permissions. That holds on CI's JDK 17 and 21 but not from JDK 24 on, where checkPermission always throws; say so next to the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Both statements can never execute, so the 100% line/branch coverage gate
cannot be met while they exist. Behavior is unchanged for every input.
- SaxEventRecorder.recordEvents() ended in
`throw new IllegalStateException("This point can never be reached")`
after its catch blocks. Every catch block already ended the method,
because handleError() always throws, but javac could not see that.
handleError() now returns the JoranException and each catch block
throws it, so the dead statement is gone. The exceptions and status
messages are the same.
- ElementPath(String) checked `if (partArray == null) return;` after
String.split(), which never returns null.
SaxEventRecorderHandlerTest drives each recordEvents() failure path
(EOF, I/O, SAX, runtime, parser configuration) through a Mockito
construction mock of the parser. It also covers the handler callbacks.
ElementPathTest pins how the string constructor splits paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Closes the unit-test coverage gaps in GenericConfigurator, BodyEvent, SaxEvent, StartEvent, ConfigurationWatchList, ConsoleTarget, ElementPath, ElementSelector, EventPlayer, HostClassAndPropertyDouble, InterpretationContext, Interpreter, JoranException, NoAutoStartUtil and SimpleRuleStore, using plain JVM tests. What the tests pin: - GenericConfigurator: a URL that cannot be opened, and a stream that fails to close, are both reported as an error status and a JoranException. The URL is still registered as the watched URL. - Events: body text is trimmed and appended. Locators and attributes are snapshots. The toString formats are fixed. - ConsoleTarget: each stream writes to whatever System.out/System.err currently is. findByName ignores case. - ElementSelector/SimpleRuleStore: null and empty paths, case-insensitive equals/hashCode, middle (*/x/*) matching with the longest match winning, a lone "*" matching nothing, and adding a rule by class name (success, and an error status for a non-Action class). - Interpreter/InterpretationContext: missing or blank body text is not passed to actions. A failing body action is reported and later actions still run. Null action lists are ignored. The error origin includes the locator position, or NA:NA when there is none. Also covered: property trimming and null handling, and a duplicate in-play listener being warned about and notified only once. - HostClassAndPropertyDouble: equals/hashCode with null members. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
- SaxEventRecorderHandlerTest: verify the parser's validation feature is switched off; cover recordEvents(InputStream) and startDocument() with plain JVM tests so the changed SaxEventRecorder is fully covered without the Robolectric tests. - ConsoleTargetTest: check that flush() reaches the current System.out / System.err (dropping either flush call went unnoticed). - SimpleRuleStoreTest: selectors starred at only one end are not middle matches (dropping either star check in middleMatch went unnoticed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
TimeBasedArchiveRemover.findEmptyDirs() walks the matching directories deepest first so that a directory whose only child is an empty directory can be deleted right after that child. The check compared the directory with the previously found empty directory itself instead of with that directory's parent, so it could only match a duplicate entry: such a parent was never deleted in the pass that emptied it and lingered until a later cleanup. Compare with the parent of the last empty directory found. The new test fails without the fix (the emptied parent is not deleted) and also exercises the single-child case that must not match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
compress() and computeFileNameStrWithoutCompSuffix() switch over every CompressionMode constant without a default, so javac emits an implicit default branch (and, in the latter, a trailing throw of IllegalStateException) that no CompressionMode value can reach, which keeps the classes below full branch coverage. Make NONE share a default label with the code it already ran; behavior is unchanged for every mode (NONE still throws UnsupportedOperationException from compress() and leaves names unchanged in computeFileNameStrWithoutCompSuffix()). The added tests pin that behavior for all three modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Pins the remaining behavior of the rolling helpers: - Compressor: argument checks (missing source, missing ZIP entry name, existing archive), failures to create the target directory, to rename the temp archive, to delete the temp file or the source (fault injection through synchronous status listeners), stale temp files of unknown age or that cannot be deleted, and the quiet close/sweep helpers. - RenameUtil: same source and target, missing source, uncreatable target directory, failed rename on the same volume vs. the copy fallback across volumes, every outcome of areOnDifferentVolumes (pre-Java 7, no or missing parent, failed store lookup, same/different store) and renameByCopying including an undeletable source. - TimeBasedArchiveRemover: undeletable files, total size cap that only counts deleted files, a non-positive cap, and (deterministically) that an empty directory of a retained period is kept. - FileNamePattern (parse errors, equals/hashCode, null pattern, auxiliary dates and foreign converters in regexes), RollingCalendar (erroneous and half-day periodicity, weekly periods, printed periodicity), FileFinder, FileFilterUtil, FileStoreUtil, FileSorter, DateParser, IntParser, the token converters and DefaultFileProvider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
- CompressorTest: a compression that fails before its temp file exists must not warn about deleting that temp file. - TimeBasedArchiveRemoverTest: a parent that still holds another entry after its empty child is removed must not be deleted in the same pass. - DateParserTest: pin a non-GMT default time zone (restored afterwards) so that the default-zone and explicit-zone paths are told apart on UTC hosts; cover a primary date token without a time zone. - FileFilterUtilTest: extractCounter needs the stem regex to match the whole name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
SMTPAppenderBaseTest now pins the mail session built at start (host, port, localhost, authentication, the SSL+STARTTLS conflict, a missing session), append's entry checks, evaluation (sync and async sending, evaluator errors capped at MAX_ERROR_COUNT, discriminator, end of life, tracker status messages) and the message sendBuffer builds: body with layout headers/footers, subject (default, truncated, null), sender, recipients (skipped, unparsable, none), content type and charset, the updateMimeMsg hook and transport failures. Transport.send is mocked statically, so no SMTP server is needed. The new SMTPAppenderTest pins the evaluator given to the constructor, caller data extraction, fillBuffer and eventMarksEndOfLife. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Add assertions and tests for behavior that survived mutation of SMTPAppenderBase and SMTPAppender: - the SSL and STARTTLS session properties - the missing evaluator and missing layout checks (checkEntryConditions returns false, and nothing is buffered or sent) - stale buffers being removed on append - the subject being encoded with charsetEncoding - a subject that starts with a newline (truncation boundary) - deferred-processing preparation of buffered events - the default subject pattern, with no exception appended to the subject - setup of the default OnErrorEvaluator Use a é escape instead of a non-ASCII literal, so the test does not depend on the source encoding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Close the unit-test coverage gaps of the server socket appenders, the concurrent server runner, the stream client and the SSL socket, context and key store factories. The tests pin: - ServerSocketAppenderBase (its test exercised AbstractServerSocketAppender instead) and AbstractServerSocketAppender: listener settings and their forwarding to the socket factory, the default factory/listener/runner, start/stop failures reported as errors, null events ignored, events transformed and offered to every client, the client queue size - ConcurrentServerRunner: clients tracked only while they run, a failing visitor reported without skipping other clients, clients that can't be configured or executed dropped and closed, interrupt ends the loop, closing a client task closes the client - RemoteReceiverStreamClient: offer without a queue, the stream reset every OOS_RESET_FREQUENCY events, socket/IO/runtime failures reported - ServerSocketListener: display of addresses with and without a host name - SSL: every ConfigurableSSL(Server)SocketFactory overload configures the delegate's socket, SSLConfigurable(Server)Socket delegation and the below-API-24 hostname verification guard, SSLParametersConfiguration's hostnameVerification getter and cached protocol/cipher suite choice, SSLNestedComponentRegistryRules' registrations, SSLContextFactoryBean's protocol/provider, default manager factories and JSSE system properties - KeyStoreFactoryBean: re-enable its ignored tests (they pass on the JVM) and pin every failure: missing location, unknown provider or type, missing file, wrong password, an unavailable integrity algorithm (via a test JCA provider) and a stream that fails to close Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
The SSLConfigurable*Socket client-auth tests only passed true, so a delegate call that ignored its argument survived; they now also pass false. ServerSocketListener's toString had no case for an address whose display string starts with the slash (i == 0), so shifting the boundary to i > 0 survived; add one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
resize() compared against the element count (numElems) where it meant the capacity (maxSize), in two places (inherited from upstream logback and log4j): - the "nothing to do" early return fired when newSize equalled the number of elements, so resizing a buffer of capacity 4 holding 2 elements to 2 was silently ignored and it kept holding up to 4; - the copy loop wrapped the read index at numElems instead of at the end of the backing array, so a buffer from which elements had been removed with get() lost elements (copied nulls) or threw ArrayIndexOutOfBoundsException once the index ran past numElems. Both checks now use maxSize. For a full buffer (numElems == maxSize) and for one never drained (first == 0) the result is unchanged. resize() has no caller in the library; the new tests reproduce each defect and pin the rest of resize(): negative size rejected, growing and shrinking a wrapped full buffer, resizing to zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
… subst Four switches over an enum in ch.qos.logback.core.subst handle every constant of the enum, so the implicit (or empty) default branch javac emits for them can never run, and JaCoCo reports it as a missed branch: - Node.toString() on Node.Type, followed by an unreachable "return null"; - NodeToStringTransformer.compileNode() on Node.Type; - Tokenizer.tokenize() on TokenizerState, twice (per character, and at the end of the input). Reaching them would take rewriting the compiler-generated switch map by reflection. Instead the last case label now shares its body with "default:", and Node.toString() loses its dead "return null". Behaviour is unchanged for every input: each constant still selects the same code, and a null type or state still throws NullPointerException from the switch itself, as before. NodeTest pins Node.toString() for both types, with and without a default part, and the NullPointerException for a node without a type; TokenizerTest, ParserTest and NodeToStringTransformerTest keep exercising every case of the other switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Close the remaining JaCoCo gaps in CyclicBuffer, ThrowableToStringArray,
Transform, Node, NodeToStringTransformer, Parser, Token and Tokenizer:
- CyclicBuffer: constructor rejects a non-positive size, indexed get()
returns null outside [0, length) even when the backing array is full,
get() on an empty buffer returns null without corrupting the count.
- ThrowableToStringArray: messageless throwables and causes, and causes
whose frames are all shared with, or outnumber, the parent's frames
(built from fixed stack traces and checked against printStackTrace).
- Transform (new TransformTest): null, empty and safe input returned as
is, every escaped character, kept tab/CR/LF, control characters
replaced with U+FFFD, the StringBuffer overload escaping in place, and
appendEscapingCDATA with no, one, several and a trailing "]]>".
- Node (NodeTest) and Token (new TokenTest): constructors, setNext,
recursive, equals for identity, null, other class and each field,
hashCode formula and consistency with equals, toString.
- Parser: a null token list parses to null, a second default separator
and an unclosed variable are rejected with their messages.
- NodeToStringTransformer: lookup order across both property
containers, system properties and the environment (the latter via
Mockito.mockStatic of OptionHelper.getEnv), undefined keys, circular
references with and without default values, and the cycle check's
node comparison (type, payload, default part, ignoring next).
- Tokenizer: "$" not followed by "{" stays literal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…Test The expected strings held a literal U+FFFD, which reads like an encoding error and depends on the source being decoded as UTF-8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Add LoggerApiTest, a plain-JVM complement to the Robolectric-run LoggerTest, so every line and branch of ch.qos.logback.classic.Logger is covered. The tests pin: - each trace/debug/info/warn/error overload (plain, Marker and List<Marker> variants) appends an event with its level, message, arguments, throwable and markers; - the ACCEPT/NEUTRAL/DENY handling of turbo filter replies for 0-, 1- and 2-argument calls, and that isXxxEnabled(Marker) asks the filters about that marker at the matching level; - isXxxEnabled/isEnabledFor reject a reply that is none of ACCEPT/NEUTRAL/DENY with "Unknown FilterReply value: null"; - log(List<Marker>, fqcn, ...) and log(slf4j LoggingEvent) map the level, carry markers/arguments/throwable and use the given caller boundary; - appender queries on loggers with and without appenders, the effective level int, and createChildByLastNamePart/createChildByName naming and separator checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
The overload tests checked level, message, arguments, throwable and markers, but not the caller data, so passing a wrong fqcn from any trace/debug/info/warn/error overload went unnoticed. Each appended event must now name the test method as its caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
ThrowableProxy looked up Throwable#getSuppressed reflectively so that it could run on Java < 7. The library's floor is Java 8 / Android API 21, where getSuppressed() always exists (API 19+), so the lookup can never fail: the NoSuchMethodException catch, the "method is null" branch and the IllegalAccessException catch (the method is public) were unreachable, which kept the class below the 100% coverage gate. Call getSuppressed() directly, as upstream logback 1.3 does, and keep a null guard for mocked throwables (a Mockito mock returns null for it). Behavior is unchanged for every real Throwable: getSuppressed() is final and never throws. The new tests pin the null-array guard with a mocked throwable and the ordering and common-frame count of proxied suppressed throwables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Bring every class of ch.qos.logback.classic.spi to 100% line and branch coverage: - CallerData: framework frames (fqcn, org.slf4j.Logger*, log4j Category, listed framework packages) are skipped, maxDepth truncates, a null throwable yields null, naInstance() fields. - ClassPackagingData, StackTraceElementProxy: constructors, the set-once packaging data, and every branch of equals/hashCode. - EventArgUtil, LoggingEvent: a trailing throwable argument becomes the event's throwable and is trimmed from the arguments; every set-once setter throws on a second call; the MDC map is copied from a foreign MDCAdapter; toString, getMdc, context birth time; the private writeObject hook refuses serialization. - PackagingDataCalculator: suppressed frames, a proxy without suppressed array, class resolution through the context class loader, a null or failing one, and Class.forName (including its NoClassDefFoundError and unexpected failures), array classes without package, code sources without location, with Windows-style or folder locations, or failing to render. - ThrowableProxy, ThrowableProxyUtil, STEUtil: packaging data is calculated once, build(), common-frame counting when either array runs out, packaging-data suffixes, the deprecated subjoinSTEPArray. - TurboFilterList: empty chain, first DENY/ACCEPT wins, all-NEUTRAL, and a single filter removed concurrently. - LoggerRemoteView: keeps the name and the context's remote view; its assert fires on a context without a remote view (Gradle runs tests with -ea). PackagingDataCalculatorTest now also restores the thread's context class loader after each test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
- PackagingDataCalculatorTest: pin the idx==0 boundary of the code location parser, the "na" version of a class without Implementation-Version, and that the expected lookups (no or unaware context class loader) report nothing on System.err. The stderr capture now records only the calculating thread's output, so stray output from other threads cannot flake the assertions. - StackTraceElementProxyTest: an instance of a subclass is not equal. - LoggerRemoteViewTest: load a copy of LoggerRemoteView with its assertion status set explicitly, so both outcomes of the assert are tested and covered whatever the JVM's -ea/-da flags are, instead of skipping one test via Assume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…ack events fallbackConfiguration() gets its events from removeIncludeEvents(), a private method that always returns a new (possibly empty) ArrayList, so the "failsafeEvents == null" operand can never be true and its branch can never be covered. Only the isEmpty() check is kept; behavior is unchanged for every input. ReconfigureOnChangeTaskJvmTest drives run() synchronously on the plain JVM and pins both outcomes of that check: an unreadable main XML file without a registered safe configuration warns "No previous configuration to fall back on.", and with one the safe events are replayed (root level restored) and registered again. It also pins the other run() paths the Robolectric ReconfigureOnChangeTaskTest leaves uncovered: a missing, null or empty watch list, a Groovy main URL, a main URL that is neither XML nor Groovy with no listeners, and a safe configuration that itself fails (reported as an error with its exception). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
These branches can never run, so no test can cover them; each change keeps the behavior for every input: - Introspector.getPropertyDescriptors: the `else if (isGet)` sits inside `if (isGet || isSet)` after `if (isSet)`, so it is always true; it becomes a plain `else`. - PropertySetter.computeAggregationType: computeRawAggregationType() only returns NOT_FOUND, AS_BASIC_PROPERTY or AS_COMPLEX_PROPERTY, so the *_COLLECTION case (which only logged "Unexpected AggregationType") and the implicit default were dead; AS_COMPLEX_PROPERTY becomes the default. - PropertySetter.isUnequivocallyInstantiable: Constructor.newInstance() returns a new instance or throws, never null, so the null check goes. - StringToObjectConverter.getValueOfMethod: Class.getMethod() throws SecurityException only under a SecurityManager, which Android ignores and JDK 18+ refuses to install; the two identical catch blocks become one multi-catch. IntrospectorTest, StringToObjectConverterTest and PropertySetterTest pin the affected methods (getter/setter detection, aggregation types, concrete-type instantiation, valueOf lookup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
… definers - ConfigurationWatchListUtilTest: null context (no list, console message instead of a status), a context without status manager, creating vs clearing the registered watch list, info/warn statuses of addToWatchList. - IntrospectionExceptionTest, PropertyDescriptorTest: constructors and accessors. - FileExistsPropertyDefinerTest, ResourceExistsPropertyDefinerTest: "true"/"false" for present/absent file or class path resource, and the error status plus null value when path/resource is unset or empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Mutation checks found three changes to covered code that no test caught: - PropertySetter.setProperty: dropping the "arg == null" check. A boolean property set to "maybe" must warn "Conversion to type [boolean] failed." and leave the target unchanged. - Introspector.decapitalize: "length() > 1" shifted to "> 2". "AB" must become "aB". - Introspector.getPropertyDescriptors: letting the getter's return type replace the setter's. Whether that happened depended on the order of Class.getMethods() for a single class. Two beans that inherit the getter or the setter now present the accessors in both orders, and the setter's type must win in each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
autoConfig() passed a local `verbose = true` to its private search helpers, whose `if (updateStatus)` checks therefore could never be false, and it guarded the first (system property) search with `if (!configured)` right after setting `configured = false`. Those branches could never be taken, so the 100% branch coverage gate could not be met while they existed. The helpers now always report the result of each resource search (as they always did), and the system property is searched unconditionally (as it always was). Behavior is unchanged: the same status messages are added, the system property still wins over assets/logback.xml, and a JoranException from configuring it still propagates without the assets being searched. ContextInitializerJvmTest runs on the plain JVM: it injects a class loader standing in for the APK and mocks JoranConfigurator's construction to pin which URL each search path (file, directory, URL, class-path resource, unknown name, assets, nothing found) hands to the configurator, and the statuses reported along the way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Bring the classic joran actions and ch.qos.logback.classic.util classes to 100% line and branch coverage with plain-JVM tests: - ConditionalIncludeAction: FileNotFound/UnknownHost/no exception are reported as info, other failures as warnings with their cause; an include is skipped only when the State on the stack holds a URL. - FindIncludeAction: end() ignores an empty stack or a foreign top object, reports "no paths found", and reports a JoranException from including the found path. - ConfigurationAction: the logback.debug system property overrides the debug attribute, "false"/"null" do not enable debug, scan="false" schedules nothing, scanning without a main URL warns, a missing, malformed or unconvertible (IllegalStateException, injected) scan period falls back to the default, and getSystemProperty() returns null when the installed Properties deny access. - ContextNameAction, LevelAction, LoggerAction, RootLoggerAction, LoggerContextListenerAction, ReceiverAction: error paths (missing name/class, uninstantiable class, level outside a logger, with the line/column in the message), INHERITED/NULL levels, and the warning when the pushed object is no longer on top of the stack; listeners and receivers are given the context, started and registered. - ContextSelectorStaticBinder: key check, JNDI refusal, a selector class named by logback.ContextSelector, and its failures. - LogbackMDCAdapter: null keys, in-place vs copy-on-write removal, getKeys/getCopyOfContextMap without a map, and the deque methods. - DefaultNestedComponentRules, LevelToSyslogSeverity, LoggerNameUtil: the registered defaults, OFF/ALL rejection, getFirstSeparatorIndexOf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Strengthen tests that let mutations survive: LoggerContextListenerAction's info statuses, RootLoggerAction's "Setting level of ROOT logger" status (and a root without a level attribute), LogbackMDCAdapter.getKeys() marking the map as handed out, and get(null) ignoring a null key that setContextMap() copied in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Close the JaCoCo gaps of EvaluationException, EventEvaluatorBase, EncoderBase, LayoutWrappingEncoder, AbstractMatcherFilter, EvaluatorFilter, Filter, DefaultShutdownHook, ShutdownHookBase, CyclicBufferAppender, OnErrorConsoleStatusListener, OnPrintStreamStatusListenerBase, StatusBase and StatusUtil. The tests pin: - EvaluatorFilter: start fails with an error status without an evaluator; decide() is NEUTRAL unless both filter and evaluator are started, maps matches/mismatches to onMatch/onMismatch, and turns an EvaluationException into NEUTRAL plus an error status carrying it. - LayoutWrappingEncoder: null header/footer without a layout, header and footer composition in the configured charset, isStarted() always false, and the deprecated immediateFlush being pushed to an OutputStreamAppender parent (warning) or reported as an error for any other parent. - DefaultShutdownHook/ShutdownHookBase: the delay is reported and slept, an interrupted sleep still stops the context, and only a ContextBase gets stopped. - OnPrintStreamStatusListenerBase: prefix, stop, and the retrospective threshold (zero disables replay; older statuses are skipped). - StatusBase equals/hashCode/toString edge cases, StatusUtil null status managers/lists, add* helpers, error/warning-free checks and cause-chain exception lookup, plus lifecycle getters/setters of the remaining classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Guard the interrupted-sleep test with a JUnit timeout so that a regression (the pending interrupt no longer cutting the one-hour delay short) fails in 30 seconds instead of blocking the build for an hour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…witches setProperty() and setProperties() switch over every ActionUtil.Scope constant, so the implicit default branch of each switch can never be taken and the 100% branch-coverage gate can never be met for them. It cannot be reached from a test either: depending on the javac version CI builds with, the switch dispatches on Scope.ordinal() directly (JDK 21) or through a synthetic $SwitchMap array (JDK 17), and forcing an unmapped value means tampering with that compiler-specific code. Label the SYSTEM case "default" too. Both labels jump to the same code, so JaCoCo counts three branches (one per constant), all reachable. Behavior is unchanged for every Scope, and a null scope still throws NullPointerException. ActionUtilTest pins what each scope writes (substitution, context or system properties, and nothing else) for both methods, and that a null scope is rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…ion catch
filePathAsURL() converts an existing file with file.toURI().toURL() and
caught MalformedURLException with printStackTrace() ("impossible to get
here"). A File's URI always has the "file" scheme, whose URL handler is
built in and cannot be overridden, so the catch could never run. The only
way to reach it from a test would be to mock the construction of
java.io.File, which on that thread also intercepts the class loader's own
File lookups and can silently drop class-path entries for the rest of the
test JVM.
Let filePathAsURL()/getInputURL() declare the exception and handle it in
begin() alongside JoranException, so that no unreachable catch (and no
printStackTrace() to stderr) remains. Every reachable input behaves as
before: missing files and directories are still reported by the existence
check, and url/resource handling is untouched.
AbstractIncludeActionTest pins, through a recording subclass, how
file/url/resource sources are resolved (with variable substitution), the
missing/duplicate source errors, optional includes staying silent,
warn-vs-error classification in handleError(), close(), and the
IllegalStateException when a source attribute vanishes after the check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…-ref and conversion-rule actions Plain JVM tests that bring these core.joran.action classes to full line and branch coverage on their own: - AbstractEventEvaluatorAction: the evaluator is created from the class attribute or the default class, named, bound to the context and pushed; missing class/name and uninstantiable classes are errors that disable end(); end() starts the evaluator and registers it in the evaluator map, warns when another object is on top of the stack, and reports a missing or read-only map. - AppenderAction: appenders are created, named (with substitution), bagged and pushed; missing names are warned about, ConsoleAppender is flagged as deprecated, and missing classes, uninstantiable classes and a missing appender bag are errors (the latter two abort with ActionException); end() starts and pops the appender or warns when it is not on top. - AppenderRefAction: the referenced appender is attached to the AppenderAttachable on top; a wrong top object, a missing ref and an unknown appender are errors. - ConversionRuleAction: rules create or extend the rule registry; missing attributes and a registry that cannot be updated are errors. - Action: line/column come from the interpreter's locator (-1 without one); body() is a no-op. ActionConst: the shared constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…n actions Mutation checks showed some covered lines were not pinned by any assertion: the reset of attributeInUse/evaluator/appender at the start of begin(), variable substitution of the url and resource include attributes, and isEmpty() (as opposed to null) checks on attributes. Add tests that fail under each of those mutations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Its 3-second JUnit timeout covered Robolectric's sandbox startup as well as the socket round trips, and timed out once on CI (JDK 21, jdk8 variant) before a retry passed. The test uses no Android API: run it on the plain JVM, and give the timeout, which only guards against hangs, 10 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
javac compiles the switch over CompressionMode in rollover() with an implicit default branch. NONE, GZ and ZIP each have a case, so that branch can never run and JaCoCo reports it as a missed branch that no test can cover. Putting the default label on ZIP's body leaves one branch per mode. Behavior is the same for every CompressionMode. FixedWindowRollingPolicyTest covers rollover in all three modes, plus the start() validation paths, window clamping, deletion of the archive at maxIndex, and rollover with a negative maxIndex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Unit tests for the remaining gaps in ResilientOutputStreamBase, ResilientFileOutputStream, ResilientSyslogOutputStream, RollingFileAppender, RollingPolicyBase, RolloverFailure, SizeAndTimeBasedFNATP, SizeAndTimeBasedRollingPolicy, SizeBasedTriggeringPolicy, TimeBasedFileNamingAndTriggeringPolicyBase and TimeBasedRollingPolicy. The tests check: - recovery: single-byte and array write failures are reported. Writes are dropped while the stream backs off, and recovery runs once the back-off has elapsed. Recovery still happens when closing the broken stream fails, and a failed reopen is reported. A stream with no context prints one console warning, and a stream whose context has no status manager drops statuses. A failed fsync is reported. The syslog stream sends flushed bytes over loopback UDP and reopens to the same host and port. - RollingFileAppender: start is refused without a triggering or rolling policy. stop() works when either policy is missing. A failed rollover keeps appending to the active file instead of truncating it. A failure to reopen the active file is reported. Two patterns only collide when they are the same. - time and size policies: missing FileNamePattern, date token and maxFileSize are rejected. cleanHistoryOnStart removes expired archives. stop() reports compression and clean-up jobs that time out or fail. An unreadable active file does not set the initial period; this test needs a non-root user and is skipped (assumption) when run as root. Compressed archives advance the counter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
FixedWindowRollingPolicyTest now covers a window of exactly the maximum size, a subclass raising getMaxWindowSize(), and a single-index window at 0, so that shifting the maxIndex/minIndex, window-size or maxIndex >= 0 comparisons makes a test fail. ResilientOutputStreamBaseTest checks that the console warning appears on the first status dropped for lack of a context and not on later ones; before, printing it only on the second status went unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
In FormatInfo.valueOf, minPart is assigned on both branches of the dot check (the prefix before the dot, or the whole string), so the `minPart != null` test can never be false. A non-null maxPart is the suffix after the dot, and a string ending in '.' is rejected with an IllegalArgumentException before it is assigned, so `maxPart.length() > 0` can never be false either. Both dead operands left branches that no input can reach, which the 100% branch coverage gate cannot accept. Removing them keeps the result identical for every input: an empty min part still leaves the default min and left padding, a missing max part still leaves the default max and left truncation, and a trailing '.' is still rejected. FormatInfoTest now pins those cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Adds unit tests so that every line and branch of ch.qos.logback.core.pattern (and its color and util subpackages) is covered: - Converter/FormattingConverter: setNext/setFormattingInfo reject a second call and keep the first value; null conversions are padded to a positive minimum and otherwise append nothing; truncation and padding directions. - CompositeConverter: child chain output is transformed; toString with and without formatting info and children. - ConverterUtil: findTail on null/single/chained converters, starting and setting the context along the chain. - DynamicConverter: first option for null, empty and filled option lists; every add* method records the right status type, message, throwable and origin. - FormatInfo: null input, equals on each field, hashCode, toString. - PatternLayoutBase: effective converter map precedence (default < context registry < instance) including a null default map and no context, starting without a context, the deprecated setContextForConverters, toString and the pattern-as-header logic. - PatternLayoutEncoderBase: header flags, deprecated setter warning, and setLayout being unsupported. - ReplacingCompositeConverter: errors for missing/too few options, pass through when not started, regex replacement when started. - Color converters: each one's ANSI code and the wrapped output, plus the ANSIConstants values. - RegularEscapeUtil: every escape case, the illegal-escape message and basicEscape translations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…hook Kill mutants the earlier tests let through: FormattingConverter's `0 < min` guard (a negative or a minimum of one with a null conversion), the ScanException branch of PatternLayoutBase.start and the post-compile processor call. Check each padded char in SpacePadderTest instead of trimming. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Pin the remaining behavior of the sifting and component-tracking code: - AbstractComponentTracker: stale components are removed before their timeout, allComponents() returns live and lingering components, the timeout/max getters, and the internal entry's equals/hashCode/toString. - AppenderAttachableImpl: null arguments are rejected or ignored, attachment is by identity, unknown names detach nothing. - ContextAwareBase/ContextAwareImpl: context re-set rules, every status flavor with its origin, the one-time "No context given" console warning and a context without status manager. - CyclicBufferTracker buffer size, FilterAttachableImpl chain decisions. - AppenderTracker: a failing or null factory yields a started NOPAppender and the NOPAppender errors are capped at MAX_ERROR_COUNT; stopped appenders are removed before they time out. - SiftingAppenderBase: settings reach the tracker, stop() stops live and lingering nested appenders, append() is a no-op when not started. - SiftingJoranConfiguratorBase one-and-only-one check and its error cap, AbstractAppenderFactoryUsingJoran drops the enclosing <sift> element, discriminator lifecycle/keys/default values, SiftAction hand-over of the recorded events, SiftingAppender end-of-life marker detection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Mutation testing showed three gaps in the new tests: - ContextAwareBase.addWarn/addError/addInfo(msg, ex) could report `this` instead of the declared origin, since only addInfo(msg) was checked with a distinct declared origin. - An overridden ContextAwareBase.getDeclaredOrigin() or ContextAwareImpl.getOrigin() was never exercised. - SiftingAppenderBase.append() could ignore the event timestamp when refreshing a nested appender's access time. Check all six status flavors against the declared and the overridden origin, and add a test in which a busy nested appender outlives an idle one that times out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
NestedBasicPropertyIA.isApplicable() and NestedComplexPropertyIA.isApplicable() switch on the AggregationType computed by PropertySetter and list all five constants, so javac's switch map sends every value to a case and the default branch (an "unexpected type" error followed by "not applicable") can never run. Reaching it would take rewriting javac's synthetic switch map through reflection, i.e. testing compiler internals, not behavior. Put the default label on the "not applicable" case, as done for TokenStream's switches. Every AggregationType keeps its behavior: the basic types (resp. complex types) push their action data and return true, the others return false without a status message. The added tests pin that for each type and for an empty object stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Plain-JVM tests that drive the actions directly (no XML parsing, which needs Robolectric's xmlpull) and bring DefinePropertyAction, IncludeAction, NestedBasicPropertyIA, NestedComplexPropertyIA, NewRuleAction, ParamAction, PropertyAction, ShutdownHookAction, StatusListenerAction and TimestampAction to full line and branch coverage: - DefinePropertyAction: a LifeCycle definer is started, a null value defines nothing, a foreign object on top of the stack is reported. - IncludeAction (recorder scripted via createRecorder): <included> and <configuration> wrappers are trimmed only when the closing element matches, a lone opening wrapper leaves nothing, null first/last events, the local name is used when the qualified name is empty, open and recording failures are reported (not when optional) and the stream is closed. - NestedBasicPropertyIA/NestedComplexPropertyIA: properties are set or added after substitution, nested components are instantiated (declared or implicit class), given the context and their parent, started unless @NoAutoStart; missing/uninstantiable classes, a foreign stack top and unexpected aggregation types are reported. - NewRuleAction: the rule reaches the interpreter's rule store; missing attributes and a failing store are reported; errors reset per element. - ParamAction, PropertyAction: missing name/value, the deprecated <substitutionProperty>, unreadable file/resource (injected IOException), missing resource and every invalid attribute combination. - ShutdownHookAction: a declared hook is registered with the JVM (and removed), a bad class is reported and rethrown, a foreign stack top registers nothing. - StatusListenerAction: context-aware LifeCycle listeners are started, plain or rejected ones are not; missing class (with line number), bad class and foreign stack top are reported. - TimestampAction: context birth vs interpretation time as reference, missing key or date pattern set no property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
…sets TimestampActionTest built its expected value with the default locale, but CachingDateFormatter formats in Locale.US, so the test failed under a Thai (Buddhist calendar) or Arabic (non-ASCII digits) default locale. It now expects the fixed "2001.123", and the "now" test checks that the value is the time of the call rather than only differing from the birth time. Also pin behavior that no test pinned before: DefinePropertyAction, ShutdownHookAction and StatusListenerAction forget an earlier element's error on the next begin(), and IncludeAction adds an included file to the configuration watch list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
TimeBasedFileNamingAndTriggeringPolicyBase.start() ignores the active file's modification time when the file can't be read. The only test of that branch withdrew read permission with setReadable(false), which has no effect for root or on Windows, so there it was skipped and the coverage gate would fail. start() now gets the file from a package-private newActiveFile(), and the test supplies a File that reports it can't be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
Routing subAppend() through writeOut() (7226724) made prudent mode lock the file, but FileAppender.safeWrite() takes the FileChannel lock without holding the appender's lock, so when two threads log at once the second lock() throws OverlappingFileLockException and the event is dropped. Upstream logback hit the same regression after 1.3.0 and fixed it by taking the stream lock around the file lock; that change to prudent mode belongs in its own pull request. Restore the 1.2 behavior and cover writeOut() and safeWrite() by calling them directly. OutputStreamAppenderTest drops eventsAreWrittenThroughWriteOut and calls writeOut() directly instead: the encoded event is written and flushed, an empty or null encoding writes nothing, and a write failure is thrown to the caller. FileAppenderTest's prudent-mode tests call FileAppender.writeOut() through FileAppenderFriend, and prudentAppenderKeepsEveryEventLoggedConcurrently has two threads log to a prudent appender with their first events overlapping, which drops one thread's events with the 7226724 behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
run()'s catch (InterruptedException) was covered only when a test's stop() happened to interrupt the receiver while it waited for its connector, so on one CI run (jdk8 variant) the coverage gate missed that line. Interrupt the receiver's thread before it waits for a connector that cannot finish, so the wait is interrupted on every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig
tony19
marked this pull request as ready for review
September 29, 2026 11:29
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This extends the coverage gate from #477, which covered only the Kotlin Android layer, to the whole library. CI now fails unless the unit tests cover every line and every branch of every class, Java and Kotlin. It also adds the tests that meet the gate.
These numbers hold for both flavors (
jdk11Debug,jdk8Debug) on both CI JDKs (17 and 21).Build (
build:)coverageReport<Variant>,coverageVerification<Variant>andverifyCoveragereplace theandroidLayer*tasks. They coverch.qos.logback.**andorg.slf4j.impl.**with the same rule as before: 100% of each class's lines and branches.scripts/coverage-gaps.py: lists what a JaCoCo XML report leaves uncovered, one file per line..github/copilot-instructions.mdand thephase-startskill now mentionverifyCoverage.Tests (
test:)SocketNode,SocketReceiver,AsyncAppenderBase,DefaultSocketConnectorandTimeBasedArchiveRemover. They now have deterministic tests, so the gate can't flake.SocketAppenderMessageLossTesttimed out once on CI inside its 3 s budget. It now runs on the plain JVM with a 10 s hang guard.SMTPAppenderBaseTestreads the part's content type without namingjavax.activation.DataHandler, which implementsjava.awt.datatransfer.Transferable.LoaderTest's permission tests rely onAccessControllerworking, which it does on CI's JDK 17 and 21. A comment next to them notes that JDK 24+ will need attention.Bugs found and fixed (
fix:)Each fix has a test that fails without it.
CyclicBuffer.resize(): it copied nulls, or threwArrayIndexOutOfBoundsException, on a buffer thatget()had partly drained. Resizing to the current element count was also ignored. The same bug is in upstream.OptionTokenizer: a pattern whose quoted option ends in an escape, such as%x{'a\', threwStringIndexOutOfBoundsExceptionout ofPatternLayoutBase.start(). It now throwsScanException, which becomes an error status.TimeBasedArchiveRemover.findEmptyDirs(): when cleanup deleted a directory's only child, it left the now-empty parent until a later pass. It now removes it in the same pass.SimpleSocketServer.doMain(): it ignored itsserverClassargument, soSimpleSSLSocketServer.mainstarted a server without SSL. The divergence from upstream is commented at the site.Unreachable code removed (
refactor:)Where a branch could not be reached by any input, subclass, configuration or mock, it was removed or folded, without changing behavior or public/protected API:
default:branches of switches that cover every constant were folded into the last case:Compressor,FixedWindowRollingPolicy,TokenStream, the substNode/Tokenizer/NodeToStringTransformer,ActionUtil,NestedBasicPropertyIA/NestedComplexPropertyIA,PropertySetter. This form covers fully under both javac 17 and javac 21, which compile enum switches differently.ElementPath,FormatInfo,ReconfigureOnChangeTask,PropertySetter,Introspector.OptionHelper:SecurityException. Android has no SecurityManager, and JDK 18+ won't install one at runtime.HardenedObjectInputStream: merged its duplicate reflection catches.StringToObjectConverter: merged two identical catch blocks.AbstractIncludeAction: aMalformedURLExceptioncatch that only printed a stack trace. The exception is now handled together withJoranExceptioninbegin().SaxEventRecorder: the "can never be reached" throw.ThrowableProxy: callsThrowable.getSuppressed()directly instead of through reflection (API 19+, as upstream 1.3 does).ContextInitializer: dropped its always-true search flags.Test seams (package-private, no public API change)
AbstractSocketAppender: the SSL handshake-failure delay is a field with a package-private setter, so tests don't sleep 30 s. The dispatch loop moved into its caller so that JaCoCo can mark it covered.TimeBasedFileNamingAndTriggeringPolicyBase.newActiveFile(): lets a test supply an unreadable active file. Before, the only test for that branch withdrew read permission, which does nothing for root or on Windows, so the gate would have depended on who ran it.Tried and reverted
OutputStreamAppender.subAppend()throughwriteOut(), as upstream 1.3.0 does, would make prudent mode actually lock the file. The final review showed this drops events when two threads log at once:FileAppender.safeWrite()takes theFileChannellock without holding the appender's lock, so the second thread getsOverlappingFileLockException. It also lets an interrupt close the channel for good. Upstream fixed this later by locking the stream around the file lock. That behavior change belongs in its own PR, so the 1.2 behavior stays, andwriteOut()/safeWrite()are covered by calling them directly.FileAppenderTest.prudentAppenderKeepsEveryEventLoggedConcurrentlypins the restored behavior: with the reverted change it loses half of the events, every run.Architecture: no package, dependency or published-output changes;
docs/architecture/is untouched.Deferred work considered:
🤖 Generated with Claude Code
https://claude.ai/code/session_0157UjTSjJyouKBfY7Zwxeig