All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.truffle.tck.tests.TestContext Maven / Gradle / Ivy

/*
 * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.tck.tests;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Instrument;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.tck.InlineSnippet;
import org.graalvm.polyglot.tck.LanguageProvider;
import org.graalvm.polyglot.tck.Snippet;
import org.graalvm.polyglot.tck.TypeDescriptor;
import org.junit.Assert;

import com.oracle.truffle.tck.common.inline.InlineVerifier;

final class TestContext implements Closeable {
    private static final Object contextCacheLock = new Object();
    private static RefCountedContextReference contextCache;

    private Map providers;
    private final Map> valueConstructors;
    private final Map> expressions;
    private final Map> statements;
    private final Map> scripts;
    private final Map> inlineScripts;
    private final boolean printOutput;
    private final boolean enableInlineVerifier;
    private Context context;
    private InlineVerifier inlineVerifier;
    private State state;

    TestContext(final Class testClass) {
        state = State.NEW;
        this.valueConstructors = new HashMap<>();
        this.expressions = new HashMap<>();
        this.statements = new HashMap<>();
        this.scripts = new HashMap<>();
        this.inlineScripts = new HashMap<>();
        boolean verbose = true;
        String propValue = System.getProperty("tck.verbose");
        if (propValue != null) {
            verbose = Boolean.parseBoolean(propValue);
        }
        propValue = System.getProperty(String.format("tck.%s.verbose", testClass.getSimpleName()));
        if (propValue != null) {
            verbose = Boolean.parseBoolean(propValue);
        }
        printOutput = verbose;
        propValue = System.getProperty("tck.inlineVerifierInstrument");
        enableInlineVerifier = propValue == null ? true : Boolean.parseBoolean(propValue);
    }

    Map getInstalledProviders() {
        checkState(State.NEW, State.INITIALIZED);
        if (providers == null) {
            state = State.INITIALIZING;
            try {
                providers = getInstalledProvidersForEngine(getContext().getEngine());
            } finally {
                state = State.INITIALIZED;
            }
        }
        return providers;
    }

    private static Map getInstalledProvidersForEngine(Engine engine) {
        final Map tmpProviders = new HashMap<>();
        final Set languages = engine.getLanguages().keySet();
        for (LanguageProvider provider : ServiceLoader.load(LanguageProvider.class)) {
            final String id = provider.getId();
            if (languages.contains(id) || isHost(provider)) {
                tmpProviders.put(id, provider);
            } else {
                throw new IllegalStateException("Provider " + provider.getClass().getName() + " requires a non installed language " + id + "\n" +
                                "Installed languages: " + String.join(", ", languages));
            }
        }
        return Collections.unmodifiableMap(tmpProviders);
    }

    @Override
    public void close() {
        checkState(State.NEW, State.INITIALIZED);
        state = State.CLOSED;
        if (context != null) {
            synchronized (contextCacheLock) {
                contextCache.close();
                if (!contextCache.isValid()) {
                    context.close();
                    contextCache = null;
                }
            }
        }
    }

    Context getContext() {
        checkState(State.NEW, State.INITIALIZING, State.INITIALIZED);
        if (context == null) {
            synchronized (contextCacheLock) {
                if (contextCache != null) {
                    this.context = contextCache.retain();
                    try {
                        contextCache.out().setDelegate(printOutput ? System.out : NullOutputStream.INSTANCE);
                        contextCache.err().setDelegate(printOutput ? System.err : NullOutputStream.INSTANCE);
                    } catch (IOException ioe) {
                        throw new RuntimeException("Failed to flush stdout, stderr.", ioe);
                    }
                } else {
                    ProxyOutputStream out = new ProxyOutputStream(printOutput ? System.out : NullOutputStream.INSTANCE);
                    ProxyOutputStream err = new ProxyOutputStream(printOutput ? System.err : NullOutputStream.INSTANCE);

                    Map knownProviders;
                    // Get known providers from a dummy context
                    try (Engine dummyCtx = Engine.newBuilder().build()) {
                        knownProviders = getInstalledProvidersForEngine(dummyCtx);
                    }
                    Context.Builder builder = Context.newBuilder();
                    knownProviders.forEach((id, provider) -> {
                        Map languageOptions = provider.additionalOptions();
                        checkAdditionalOptions(languageOptions, provider.getId());
                        builder.options(languageOptions);
                    });
                    this.context = builder.allowExperimentalOptions(true).allowAllAccess(true).out(out).err(err).build();

                    assert contextCache == null;
                    contextCache = new RefCountedContextReference(context, out, err);
                }
            }
            if (enableInlineVerifier) {
                Instrument instrument = context.getEngine().getInstruments().get(InlineVerifier.ID);
                this.inlineVerifier = instrument.lookup(InlineVerifier.class);
                Assert.assertNotNull(this.inlineVerifier);
            }
        }
        return context;
    }

    Collection getValueConstructors(TypeDescriptor type, String... ids) {
        Objects.requireNonNull(ids);
        checkState(State.NEW, State.INITIALIZED);
        return filter(
                        valueConstructors,
                        LanguageIdPredicate.create(ids),
                        TypePredicate.create(type, null),
                        new Function>() {
                            @Override
                            public Collection apply(LanguageProvider tli) {
                                final Collection result = tli.createValueConstructors(context);
                                for (Snippet snippet : result) {
                                    if (!snippet.getParameterTypes().isEmpty()) {
                                        Assert.fail("Value constructors cannot have parameters, invalid Snippet: " + snippet);
                                    }
                                    if (snippet.getReturnType().isUnion()) {
                                        Assert.fail("Value constructors cannot return union types (use intersection type), invalid Snippet: " + snippet);
                                    }
                                }
                                return result;
                            }
                        });
    }

    Collection getExpressions(TypeDescriptor type, List parameterTypes, String... ids) {
        Objects.requireNonNull(ids);
        checkState(State.NEW, State.INITIALIZED);
        return filter(
                        expressions,
                        LanguageIdPredicate.create(ids),
                        TypePredicate.create(type, parameterTypes),
                        new Function>() {
                            @Override
                            public Collection apply(LanguageProvider tli) {
                                return tli.createExpressions(context);
                            }
                        });
    }

    Collection getScripts(TypeDescriptor type, String... ids) {
        Objects.requireNonNull(ids);
        checkState(State.NEW, State.INITIALIZED);
        return filter(
                        scripts,
                        LanguageIdPredicate.create(ids),
                        TypePredicate.create(type, Collections.emptyList()),
                        new Function>() {
                            @Override
                            public Collection apply(LanguageProvider tli) {
                                return tli.createScripts(context);
                            }
                        });
    }

    Collection getStatements(TypeDescriptor type, List parameterTypes, String... ids) {
        Objects.requireNonNull(ids);
        checkState(State.NEW, State.INITIALIZED);
        return filter(
                        statements,
                        LanguageIdPredicate.create(ids),
                        TypePredicate.create(type, parameterTypes),
                        new Function>() {
                            @Override
                            public Collection apply(LanguageProvider tli) {
                                return tli.createStatements(context);
                            }
                        });
    }

    Collection getInlineScripts(String... ids) {
        return filter(
                        inlineScripts,
                        LanguageIdPredicate.create(ids),
                        new Predicate() {
                            @Override
                            public boolean test(InlineSnippet s) {
                                return true;
                            }
                        },
                        new Function>() {
                            @Override
                            public Collection apply(LanguageProvider tli) {
                                return tli.createInlineScripts(context);
                            }
                        });
    }

    private  Collection filter(
                    final Map> cache,
                    final Predicate> idPredicate,
                    final Predicate typePredicate,
                    final Function> provider) {
        return getInstalledProviders().entrySet().stream().filter(idPredicate).flatMap(new Function, Stream>() {
            @Override
            public Stream apply(Map.Entry e) {
                return cache.computeIfAbsent(e.getKey(), new Function>() {
                    @Override
                    public Collection apply(String k) {
                        return provider.apply(e.getValue());
                    }
                }).stream();
            }
        }).filter(typePredicate).collect(Collectors.toList());
    }

    private void checkState(final State... allowedInStates) {
        boolean allowed = false;
        for (State allowedState : allowedInStates) {
            if (state == allowedState) {
                allowed = true;
                break;
            }
        }
        if (!allowed) {
            throw new IllegalStateException("Cannot be called in state: " + state);
        }
    }

    private static boolean isHost(LanguageProvider provider) {
        return provider.getClass() == JavaHostLanguageProvider.class;
    }

    private static void checkAdditionalOptions(Map languageOptions, String languageId) {
        String prefix = languageId + ".";
        for (String key : languageOptions.keySet()) {
            if (!key.startsWith(prefix)) {
                throw new IllegalArgumentException("Provider for language '" + languageId + "' attempts to set option " + key);
            }
        }
    }

    Value getValue(Object object) {
        return context.asValue(object);
    }

    void setInlineSnippet(String languageId, InlineSnippet inlineSnippet, InlineVerifier.ResultVerifier verifier) {
        if (inlineVerifier != null) {
            inlineVerifier.setInlineSnippet(languageId, inlineSnippet, verifier);
        }
    }

    private enum State {
        NEW,
        INITIALIZING,
        INITIALIZED,
        CLOSED
    }

    private static final class LanguageIdPredicate implements Predicate> {
        private static final Predicate> TRUE = new Predicate<>() {
            @Override
            public boolean test(Map.Entry e) {
                return true;
            }
        };
        private final Set requiredIds;

        private LanguageIdPredicate(final String... ids) {
            requiredIds = new HashSet<>();
            Collections.addAll(requiredIds, ids);
        }

        @Override
        public boolean test(Map.Entry e) {
            return requiredIds.contains(e.getKey());
        }

        static Predicate> create(String... ids) {
            return ids.length == 0 ? TRUE : new LanguageIdPredicate(ids);
        }
    }

    private static final class TypePredicate implements Predicate {
        private final TypeDescriptor type;
        private final List parameterTypes;

        private TypePredicate(TypeDescriptor type, List parameterTypes) {
            this.type = type;
            this.parameterTypes = parameterTypes;
        }

        @Override
        public boolean test(final Snippet op) {
            if (type != null && !op.getReturnType().isAssignable(type)) {
                return false;
            }
            if (parameterTypes != null) {
                List opParameterTypes = op.getParameterTypes();
                if (parameterTypes.size() != opParameterTypes.size()) {
                    return false;
                }
                for (int i = 0; i < parameterTypes.size(); i++) {
                    if (!opParameterTypes.get(i).isAssignable(parameterTypes.get(i))) {
                        return false;
                    }
                }
            }
            return true;
        }

        static Predicate create(TypeDescriptor type, List parameterTypes) {
            return new TypePredicate(type, parameterTypes);
        }
    }

    private static final class NullOutputStream extends OutputStream {
        static OutputStream INSTANCE = new NullOutputStream();

        private NullOutputStream() {
        }

        @Override
        public void write(int b) throws IOException {
        }
    }

    private static final class ProxyOutputStream extends OutputStream {

        private OutputStream delegate;

        ProxyOutputStream(OutputStream delegate) {
            Objects.requireNonNull(delegate, "Delegate must be non null.");
            this.delegate = delegate;
        }

        void setDelegate(OutputStream newDelegate) throws IOException {
            this.delegate.flush();
            this.delegate = newDelegate;
        }

        @Override
        public void write(int b) throws IOException {
            delegate.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException {
            delegate.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            delegate.write(b, off, len);
        }

        @Override
        public void flush() throws IOException {
            delegate.flush();
        }

        @Override
        public void close() throws IOException {
            delegate.close();
        }
    }

    private static final class RefCountedContextReference implements Closeable {

        private final ProxyOutputStream out;
        private final ProxyOutputStream err;
        private Context context;
        private int refCount;

        RefCountedContextReference(Context context, ProxyOutputStream out, ProxyOutputStream err) {
            this.context = context;
            this.refCount = 1;
            this.out = out;
            this.err = err;
        }

        ProxyOutputStream out() {
            return out;
        }

        ProxyOutputStream err() {
            return err;
        }

        Context retain() {
            if (refCount == 0) {
                throw new IllegalStateException("Released reference");
            }
            refCount++;
            return context;
        }

        boolean isValid() {
            return refCount > 0;
        }

        @Override
        public void close() {
            if (refCount == 0) {
                throw new IllegalStateException("Released reference");
            }
            refCount--;
            if (refCount == 0) {
                context = null;
            }
        }
    }
}