4

In writing test cases for Neo4j I would like to move onto using just the JUnit 5 Extension Model and not use org.junit.vintage or junit-jupiter-migrationsupport. Currently I can only find the Neo4j test-harness for JUnit 4 which uses TestRule and is dependent on org.junit.vintage and junit-jupiter-migrationsupport.

Is there a Neo4j test harness for JUnit 5 that uses the Extension Model?

References:
Neo4j: Home, GitHub
Neo4j test-harness: Maven, GitHub, pom.xml
JUnit 4: GitHub
JUnit 4 TestRule: JUnit 4 Guide, JUnit 4.12 API, Neo4jRule GitHub
JUnit 5: GitHub
JUnit 5 Extension Model: JUnit 5 User Guide, GitHub
JUnit 5 org.junit.vintage: JUnit 5 User Guide, Test-harness pom.xml
JUnit 5 junit-jupiter-migrationsupport: JUnit 5 User Guide, Test-harness pom.xml


I know it is possible to use JUnit 4 and JUnit 5 in a mixed environment, e.g. Mixing JUnit 4 and JUnit 5 tests.

I have started to write my own Neo4j JUnit 5 extensions with the help of A Guide to JUnit 5 Extensions but if a standard Neo4j test harness with the JUnit 5 Extension Model already exist why create my own.

It may be that I am just querying with the wrong keywords which are simply neo4j and JUnit 5 but the same results keep turning up, none of which lead to what I seek.

Checked the JUnit Jupiter Extensions and found none for Neo4j.

EDIT

Proof of concept

Since the code below is only proof of concept it is not posted as the accepted answer, but hopefully will be in a matter of days.

Turns out that adding JUnit 5 Jupiter Extensions to an existing JUnit TestRlue is not all that bad. There were a few rough spots along the way, and if you are like me and don't live and breath a single programming language or set of tools you have to take some time to understand the ethos; that should be an SO tag if you ask me.

Note: This code is a combination of some code from the Neo4j TestRule and A Guide to JUnit 5 Extensions

Starting with Neo4j TestRule just change the implements:
Remove TestRule
Add BeforeEachCallback and AfterEachCallback

Note: BeforeEach and AfterEach are used instead of BeforeAll and AfterAll with Neo4j because with each new test when creating nodes, if a new node is created the same as the previous test and the database is not a new database then checking the id of the node will be different because a new node is created for each test and gets a different id. So to avoid this problem and doing it the same way it is done with the Neo4j TestRule, a new database is created for each test instance. I did look into resetting the the database between test but it appears that the only way to do this is to delete all of the files that make up the database. :(

/*
 * Copyright (c) 2002-2018 "Neo4j,"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
//package org.neo4j.harness.junit;
package org.egt.neo4j.harness.example_002.junit;

// References:
// GitHub - junit-team - junit5 - junit5/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine - https://github.com/junit-team/junit5/tree/releases/5.3.x/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension

// Notes:
// With JUnit 4 TestRule there was basically one rule that was called at multiple points and for multiple needs.
// With JUnit 5 Extensions the calls are specific to a lifecycle step, e.g. BeforeAll, AfterEach,
// or specific to a need, e.g. Exception handling, maintaining state across test,
// so in JUnit 4 where a single TestRule could be created in JUnit5 many Extensions need to be created.
// Another major change is that with JUnit 4 a rule would wrap around a test which would make
// implementing a try/catch easy, with JUnit 5 the process is broken down into a before and after callbacks
// that make this harder, however because the extensions can be combined for any test,
// adding the ability to handle exceptions does not require adding the code to every extension,
// but merely adding the extension to the test. (Verify this).

import java.io.File;
import java.io.PrintStream;
import java.util.function.Function;

import org.junit.jupiter.api.extension.*;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.config.Setting;

import org.egt.neo4j.harness.example_002.ServerControls;
import org.egt.neo4j.harness.example_002.TestServerBuilder;
import org.egt.neo4j.harness.example_002.TestServerBuilders;

/**
 * A convenience wrapper around {@link org.neo4j.harness.TestServerBuilder}, exposing it as a JUnit
 * {@link org.junit.Rule rule}.
 *
 * Note that it will try to start the web server on the standard 7474 port, but if that is not available
 * (typically because you already have an instance of Neo4j running) it will try other ports. Therefore it is necessary
 * for the test code to use {@link #httpURI()} and then {@link java.net.URI#resolve(String)} to create the URIs to be invoked.
 */
//public class Neo4jRule implements TestRule, TestServerBuilder
public class Neo4jDatabaseSetupExtension implements  BeforeEachCallback, AfterEachCallback, TestServerBuilder
{
    private TestServerBuilder builder;
    private ServerControls controls;
    private PrintStream dumpLogsOnFailureTarget;

    Neo4jDatabaseSetupExtension(TestServerBuilder builder )
    {
        this.builder = builder;
    }

    public Neo4jDatabaseSetupExtension( )
    {
        this( TestServerBuilders.newInProcessBuilder() );
    }

    public Neo4jDatabaseSetupExtension(File workingDirectory )
    {
        this( TestServerBuilders.newInProcessBuilder( workingDirectory ) );
    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {

        if (controls != null)
        {
            controls.close();
        }
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        controls = builder.newServer();
    }

    @Override
    public ServerControls newServer() {
        throw new UnsupportedOperationException( "The server cannot be manually started via this class, it must be used as a JUnit 5 Extension." );
    }

    @Override
    public TestServerBuilder withConfig(Setting<?> key, String value) {
        builder = builder.withConfig( key, value );
        return this;
    }

    @Override
    public TestServerBuilder withConfig(String key, String value) {
        builder = builder.withConfig( key, value );
        return this;
    }

    @Override
    public TestServerBuilder withExtension(String mountPath, Class<?> extension) {
        builder = builder.withExtension( mountPath, extension );
        return this;
    }

    @Override
    public TestServerBuilder withExtension(String mountPath, String packageName) {
        builder = builder.withExtension( mountPath, packageName );
        return this;
    }

    @Override
    public TestServerBuilder withFixture(File cypherFileOrDirectory) {
        builder = builder.withFixture( cypherFileOrDirectory );
        return this;
    }

    @Override
    public TestServerBuilder withFixture(String fixtureStatement) {
        builder = builder.withFixture( fixtureStatement );
        return this;
    }

    @Override
    public TestServerBuilder withFixture(Function<GraphDatabaseService, Void> fixtureFunction) {
        builder = builder.withFixture( fixtureFunction );
        return this;
    }

    @Override
    public TestServerBuilder copyFrom(File sourceDirectory) {
        builder = builder.copyFrom( sourceDirectory );
        return this;
    }

    @Override
    public TestServerBuilder withProcedure(Class<?> procedureClass) {
        builder = builder.withProcedure( procedureClass );
        return this;
    }

    @Override
    public TestServerBuilder withFunction(Class<?> functionClass) {
        builder = builder.withFunction( functionClass );
        return this;
    }

    @Override
    public TestServerBuilder withAggregationFunction(Class<?> functionClass) {
        builder = builder.withAggregationFunction( functionClass );
        return this;
    }
}

Next, to allow each test instance to have a new GraphDatabaseService which is created with ServerControls implement a JUnit 5 ParameterResolver.

package org.egt.neo4j.harness.example_002.junit;

import org.egt.neo4j.harness.example_002.ServerControls;
import org.egt.neo4j.harness.example_002.TestServerBuilders;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

public class Neo4jDatabaseParameterResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        boolean result = parameterContext.getParameter()
                .getType()
                .equals(ServerControls.class);

        return result;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {

        Object result = (ServerControls)TestServerBuilders.newInProcessBuilder().newServer();

        return result;
    }
}

Finally all that is left is to use the Neo4j JUnit 5 Extension Model with @ExtendWith and @Test:

package org.egt.example_002;

import org.egt.neo4j.harness.example_002.ServerControls;
import org.egt.neo4j.harness.example_002.junit.Neo4jDatabaseParameterResolver;
import org.egt.neo4j.harness.example_002.junit.Neo4jDatabaseSetupExtension;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith({ Neo4jDatabaseSetupExtension.class, Neo4jDatabaseParameterResolver.class })
public class Neo4jUnitTests {

    private ServerControls sc;
    private GraphDatabaseService graphDb;

    public Neo4jUnitTests(ServerControls sc) {
        this.sc = sc;
        this.graphDb = sc.graph();
    }

    @Test
    public void shouldCreateNode()
    {
        // START SNIPPET: unitTest
        Node n;
        try ( Transaction tx = graphDb.beginTx() )
        {
            n = graphDb.createNode();
            n.setProperty( "name", "Nancy" );
            tx.success();
        }

        long id = n.getId();
        // The node should have a valid id
        assertEquals(0L, n.getId());

        // Retrieve a node by using the id of the created node. The id's and
        // property should match.
        try ( Transaction tx = graphDb.beginTx() )
        {
            Node foundNode = graphDb.getNodeById( n.getId() );
            assertEquals( foundNode.getId(),  n.getId() );
            assertEquals( "Nancy" , (String)foundNode.getProperty("name") );
        }
        // END SNIPPET: unitTest

    }
}

One import thing I learned along the way in doing this is that TestRule code seems to be a do everything in one class while the new Extension Model uses many extensions to do the same thing. Thus the logging, exception handling and other things the Neo4j TestRule have are not in this proof of concept. However because the Extension Model allows you to mix and match extensions, adding the logging and exception handling can be as easy as using an extension from another place and just adding the @ExtendWith which is why I haven't created them for this proof of concept.

Also you will have noticed that I change the package names which I did only to avoid clashes with other code in the same project that implement other parts of the code in a stand alone fashion so I could walk my way up to this working proof of concept.

Lastly, I would not be surprised if the JUnit 4 Neo4j TestRule class and a JUnit 5 Extension Model class could both inherit from a base class and then be made available in same test-harness; fingers crossed. Obviously most of the base class would be extracted from the Neo4j TestRule class.

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
  • Of interest: [JUnit 5 – The Ultimate Resource](https://www.petrikainulainen.net/junit-5-the-ultimate-resource/) – Guy Coder Oct 31 '18 at 15:47
  • Of interest: [What use is @TestInstance annotation in JUnit 5?](https://stackoverflow.com/a/52553421/1243762) – Guy Coder Oct 31 '18 at 16:30
  • Of interest: [Enabling Automatic Extension Detection](https://junit.org/junit5/docs/current/user-guide/#extensions-registration-automatic-enabling) and [junit.jupiter.extensions.autodetection.enabled](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven-config-params) – Guy Coder Oct 31 '18 at 16:43
  • Of interest: [How to reset / clear / delete neo4j database?](https://stackoverflow.com/q/23310114) – Guy Coder Oct 31 '18 at 16:52
  • Of interest: [JUnit - org.junit.jupiter.api.extension - Interface Extension](https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/extension/Extension.html) and [summary](https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/extension/package-summary.html) – Guy Coder Oct 31 '18 at 16:55
  • Of interest: [Backup Neo4j - Community Edition](https://daten-und-bass.io/blog/backup-neo4j-community-edition-plain-linux-and-docker/) – Guy Coder Oct 31 '18 at 18:03
  • Of interest: [The JUnit 5 Jupiter API - Part 1](https://www.ibm.com/developerworks/java/library/j-introducing-junit5-part1-jupiter-api/index.html) and [The JUnit 5 Jupiter API - Part 2](https://www.ibm.com/developerworks/library/j-introducing-junit5-part2-vintage-jupiter-extension-model/index.html) – Guy Coder Oct 31 '18 at 23:39
  • Of interest: The Neo4j Developer Manual v3.4 Section 4.3 [Sessions and transactions](https://neo4j.com/docs/developer-manual/current/drivers/sessions-transactions/) – Guy Coder Nov 01 '18 at 21:07
  • Of interest: [Using JUnit 5 in IntelliJ IDEA](https://blog.jetbrains.com/idea/2016/08/using-junit-5-in-intellij-idea/) – Guy Coder Nov 04 '18 at 13:52
  • Of interest: JUnit 5 [Tag Expressions](https://junit.org/junit5/docs/current/user-guide/#running-tests-tag-expressions) – Guy Coder Nov 04 '18 at 13:53
  • Of interest: [How to use JUnit 5 @Tag with IntelliJ and Maven](https://stackoverflow.com/a/51092232) – Guy Coder Nov 04 '18 at 13:53
  • Of interest: [How to specify which @Tag should be used for JUnit in IntelliJ IDEA](https://stackoverflow.com/a/50958900/1243762) – Guy Coder Nov 04 '18 at 14:01
  • Of interest: JUnit team [What are Core Principles?](https://github.com/junit-team/junit5/wiki/Core-Principles) - Covers [Prefer extension points over features](https://github.com/junit-team/junit5/wiki/Core-Principles#prefer-extension-points-over-features) – Guy Coder Nov 08 '18 at 18:08
  • 1
    There will be a Neo4jTestcontainer in the upcoming Testcontainers release. – Michael Simons Dec 17 '18 at 11:19

3 Answers3

3

The easiest way is probably not using the extension at all.

Use the following dependencies for Neo4j 4.x:

<dependency>
    <groupId>org.neo4j.test</groupId>
    <artifactId>neo4j-harness</artifactId>
    <version>4.0.8</version>
    <scope>test</scope>
</dependency>

And then structure your JUnit 5 test like this:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;

public class SimpleTest {

    private static Neo4j embeddedDatabaseServer;

    @BeforeAll
    static void initializeNeo4j() {

        embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
            .withDisabledServer() // Don't need Neos HTTP server
            .withFixture(""
                + "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})"
            )
            .build();
    }

    @AfterAll
    static void stopNeo4j() {

        embeddedDatabaseServer.close();
    }

    @Test
    void testSomething() {

        try(var tx = embeddedDatabaseServer.databaseManagementService().database("neo4j").beginTx()) {
            var result = tx.execute("MATCH (m:Movie) WHERE m.title = 'The Matrix' RETURN m.released");
            Assertions.assertEquals(1999L, result.next().get("m.released"));
        }
    }
}

Of course you can alternatively open up a bolt URL to the embedded instance. embeddedDatabaseServer.boltURI() gives you a local socket address. Authentication is turned off.

The test would look like this:

@Test
void testSomethingOverBolt() {

    try(var driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI(), AuthTokens.none());
    var session = driver.session()) {
        var result = session.run("MATCH (m:Movie) WHERE m.title = 'The Matrix' RETURN m.released");
        Assertions.assertEquals(1999L, result.next().get("m.released").asLong());
    }
}

Of course you would need org.neo4j.driver:neo4j-java-driver for that.

In case need a non-static instance of the embedded server, you could model the whole test class like this:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.neo4j.harness.Neo4j;
import org.neo4j.harness.Neo4jBuilders;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SimpleTest {

    private final Neo4j embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
            .withDisabledServer() // Don't need Neos HTTP server
        .withFixture(""
            + "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})"
        )
        .build();

    @AfterAll
    void stopNeo4j() {

        embeddedDatabaseServer.close();
    }

    @Test
    void whatever() {
    }
}

Notice the @TestInstance(TestInstance.Lifecycle.PER_CLASS) on top of the test class and the non-static @AfterAll method.

Michael Simons
  • 4,640
  • 1
  • 27
  • 38
  • I am giving this the accept vote without verifying it as this answer was given more than a year after asking. It could be years before I use such code again to verify this is the answer. If however I do find that one of the other answers is the answer to this problem I will change the accept vote. – Guy Coder Feb 16 '22 at 10:25
1

Not really an answer but I have a Neo4jExtension class which is a JUnit 5 extension.
Mine is mostly hardcoded because I wanted something that works quickly for me.
It creates an embedded Neo4j database with bolt connector.
It also loads some apoc procedures and functions and loads initial data for tests.
Your approach is more interesting.

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.internal.kernel.api.exceptions.KernelException;
import org.neo4j.kernel.configuration.BoltConnector;
import org.neo4j.kernel.configuration.Settings;
import org.neo4j.kernel.impl.proc.Procedures;
import org.neo4j.kernel.internal.GraphDatabaseAPI;

import java.io.File;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.neo4j.helpers.ListenSocketAddress.listenAddress;
import static org.neo4j.kernel.configuration.BoltConnector.EncryptionLevel.DISABLED;
import static org.neo4j.kernel.configuration.Connector.ConnectorType.BOLT;
import static org.neo4j.kernel.configuration.Settings.FALSE;
import static org.neo4j.kernel.configuration.Settings.STRING;
import static org.neo4j.kernel.configuration.Settings.TRUE;

@Slf4j
public class Neo4jExtension implements
        BeforeAllCallback, AfterAllCallback, ParameterResolver,
        BeforeEachCallback, AfterEachCallback {

    private static final File DB_PATH = new File("target/neo4j-test");

    private GraphDatabaseService graphDb;
    private Transaction currentTransaction;

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {
        FileUtils.deleteDirectory(DB_PATH);
        TomcatURLStreamHandlerFactory.disable();
        final BoltConnector boltConnector = new BoltConnector("bolt");
        graphDb = new GraphDatabaseFactory()
                .newEmbeddedDatabaseBuilder(DB_PATH)
                .setConfig(Settings.setting("dbms.directories.import", STRING, "data"),"../../data")
                .setConfig(Settings.setting("dbms.security.procedures.unrestricted", STRING, "apoc.*"),"apoc.*")
                .setConfig(boltConnector.type, BOLT.name())
                .setConfig(boltConnector.enabled, TRUE)
                .setConfig(boltConnector.listen_address, listenAddress("127.0.0.1", 7676))
                .setConfig(boltConnector.encryption_level, DISABLED.name())
                .setConfig(GraphDatabaseSettings.auth_enabled, FALSE)
                .newGraphDatabase();
        Procedures procedures = ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(Procedures.class);
        List<Class<?>> apocProcedures = asList(apoc.convert.Json.class);
        apocProcedures.forEach((procedure) -> {
            try {
                procedures.registerFunction(procedure);
                procedures.registerProcedure(procedure);
            } catch (KernelException e) {
                e.printStackTrace();
            }
        });
        final String importScript = FileUtils.readFileToString(new File("data/import_data.cql"), UTF_8);
        final String[] split = importScript.split(";");
        for (String query : split) {
            if (StringUtils.isNotBlank(query)) {
                graphDb.execute(query);
            }
        }
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) throws Exception {
        graphDb.shutdown();
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        currentTransaction = graphDb.beginTx();
    }

    @Override
    public void afterEach(ExtensionContext extensionContext) throws Exception {
        currentTransaction.failure();
        currentTransaction.close();
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
                                     ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.getParameter().getType().equals(GraphDatabaseService.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
                                   ExtensionContext extensionContext) throws ParameterResolutionException {
        return graphDb;
    }
}
grandouassou
  • 2,500
  • 3
  • 25
  • 60
  • I like the way you made effective use of `BeforeEach` and `AfterEach`. My current play version now uses `TestServerBuilders` instead of `ServerControls` because I needed lower level access. Still playing with it. While I like your answer I won't be accepting it at present because I feel this question is ahead of the curve and needs time, months maybe a year or more, so that the real test-harness can implement JUnit5 without the crutches. When the test-harness is updated, if it satisfies what I need then that would become the accepted answer. – Guy Coder Nov 16 '18 at 14:47
  • If you have another variation, now or later, then post that as an additional answer and I will give it an up-vote. This question did not lead to the answer I sought, so I hope in the mean time it serves as a place for others to find, learn from, and help to add their insight into the same goal we seem to have. – Guy Coder Nov 16 '18 at 14:49
1

Update: Folk closer to the Neo4J/Spring Data ecosystem have recommended against using Neo4jExtention. See this answer for more detail.


There is now (since ~January 2019) a Neo4jExtension for JUnit 5 that does essentially what the Neo4jRule was doing for Junit 4.

You use it like this:

@ExtendWith(org.neo4j.harness.junit.extension.Neo4jExtension.class)
class MyTest {
    ...

If you're using Spring / Spring Data Neo4j, you may find that this extension doesn't play particularly well with the SpringExtension. I've described how to resolve that in another Q&A here: How do I set up a Spring Data Neo4j integration test with JUnit 5 (in Kotlin)?

Graham Lea
  • 5,797
  • 3
  • 40
  • 55
  • As I have not used Java with JUnit for sometime, I would have to rebuild the tool chain to verify this answer to check if it is acceptable; I suspect it is correct. It may be some months before I can verify so don't think I have forgot. – Guy Coder Mar 29 '20 at 11:56