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

com.yahoo.vespa.testrunner.TestRunnerHandler Maven / Gradle / Ivy

There is a newer version: 8.444.18
Show newest version
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.testrunner;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.yahoo.component.annotation.Inject;
import com.yahoo.component.provider.ComponentRegistry;
import com.yahoo.container.jdisc.EmptyResponse;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.exception.ExceptionUtils;
import com.yahoo.restapi.MessageResponse;
import com.yahoo.vespa.testrunner.TestReport.FailureNode;
import com.yahoo.vespa.testrunner.TestReport.NamedNode;
import com.yahoo.vespa.testrunner.TestReport.Node;
import com.yahoo.vespa.testrunner.TestReport.OutputNode;
import com.yahoo.vespa.testrunner.TestReport.TestNode;
import com.yahoo.yolean.Exceptions;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import static com.yahoo.jdisc.Response.Status;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * @author valerijf
 * @author jonmv
 * @author mortent
 */
public class TestRunnerHandler extends ThreadedHttpRequestHandler {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
    private static final JsonFactory factory = new JsonFactory();

    private final TestRunner testRunner;

    @Inject
    public TestRunnerHandler(Executor executor, ComponentRegistry testRunners) {
        this(executor, AggregateTestRunner.of(testRunners.allComponents()));
    }

    TestRunnerHandler(Executor executor, TestRunner testRunner) {
        super(executor);
        this.testRunner = testRunner;
    }

    @Override
    public HttpResponse handle(HttpRequest request) {
        try {
            switch (request.getMethod()) {
                case GET: return handleGET(request);
                case POST: return handlePOST(request);

                default: return new MessageResponse(Status.METHOD_NOT_ALLOWED, "Method '" + request.getMethod() + "' is not supported");
            }
        } catch (IllegalArgumentException e) {
            return new MessageResponse(Status.BAD_REQUEST, Exceptions.toMessageString(e));
        } catch (Exception e) {
            log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
            return new MessageResponse(Status.INTERNAL_SERVER_ERROR, Exceptions.toMessageString(e));
        }
    }

    private HttpResponse handleGET(HttpRequest request) {
        String path = request.getUri().getPath();
        switch (path) {
            case "/tester/v1/log":
                long fetchRecordsAfter = Optional.ofNullable(request.getProperty("after"))
                                                 .map(Long::parseLong)
                                                 .orElse(-1L);
                return new CustomJsonResponse(out -> render(out, testRunner.getLog(fetchRecordsAfter)));
            case "/tester/v1/status":
                return new MessageResponse(testRunner.getStatus().name());
            case "/tester/v1/report":
                TestReport report = testRunner.getReport();
                if (report == null) return new EmptyResponse(204);
                else return new CustomJsonResponse(out -> render(out, report));
        }
        return new MessageResponse(Status.NOT_FOUND, "Not found: " + request.getUri().getPath());
    }

    private HttpResponse handlePOST(HttpRequest request) throws IOException {
        final String path = request.getUri().getPath();
        if (path.startsWith("/tester/v1/run/")) {
            String type = lastElement(path);
            TestRunner.Suite testSuite = TestRunner.Suite.valueOf(type.toUpperCase() + "_TEST");
            byte[] config = request.getData().readAllBytes();
            testRunner.test(testSuite, config);
            log.info("Started tests of type " + type + " and status is " + testRunner.getStatus());
            return new MessageResponse("Successfully started " + type + " tests");
        }
        return new MessageResponse(Status.NOT_FOUND, "Not found: " + request.getUri().getPath());
    }

    private static String lastElement(String path) {
        if (path.endsWith("/"))
            path = path.substring(0, path.length() - 1);
        int lastSlash = path.lastIndexOf("/");
        if (lastSlash < 0) return path;
        return path.substring(lastSlash + 1);
    }

    private static void render(OutputStream out, Collection log) throws IOException {
        var json = factory.createGenerator(out);
        json.writeStartObject();
        json.writeArrayFieldStart("logRecords");
        for (LogRecord record : log) {
            String message = record.getMessage() == null ? "" : record.getMessage();
            if (record.getThrown() != null) {
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                record.getThrown().printStackTrace(new PrintStream(buffer));
                message += (message.isEmpty() ? "" : "\n") + buffer;
            }
            json.writeStartObject();
            json.writeNumberField("id", record.getSequenceNumber());
            json.writeNumberField("at", record.getMillis());
            json.writeStringField("type", typeOf(record.getLevel()));
            json.writeStringField("message", message);
            json.writeEndObject();
        }
        json.writeEndArray();
        json.writeEndObject();
        json.close();
    }

    private static String typeOf(Level level) {
        return    level.getName().equals("html") ? "html"
                : level.intValue() < Level.INFO.intValue() ? "debug"
                : level.intValue() < Level.WARNING.intValue() ? "info"
                : level.intValue() < Level.SEVERE.intValue() ? "warning"
                : "error";
    }

    private static void render(OutputStream out, TestReport report) throws IOException {
        JsonGenerator json = factory.createGenerator(out);
        json.writeStartObject();

        json.writeFieldName("report");
        render(json, (Node) report.root());

        json.writeEndObject();
        json.close();
    }

    private static void render(JsonGenerator json, Node node) throws IOException {
        json.writeStartObject();
        if (node instanceof NamedNode) render(json, (NamedNode) node);
        if (node instanceof OutputNode) render(json, (OutputNode) node);

        if ( ! node.children().isEmpty()) {
            json.writeArrayFieldStart("children");
            for (Node child : node.children) {
                render(json, child);
            }
            json.writeEndArray();
        }
        json.writeEndObject();
    }

    private static void render(JsonGenerator json, NamedNode node) throws IOException {
        String type = node instanceof FailureNode ? "failure" : node instanceof TestNode ? "test" : "container";
        json.writeStringField("type", type);
        json.writeStringField("name", node.name());
        json.writeStringField("status", node.status().name());
        json.writeNumberField("start", node.start().toEpochMilli());
        json.writeNumberField("duration", node.duration().toMillis());
    }

    private static void render(JsonGenerator json, OutputNode node) throws IOException {
        json.writeStringField("type", "output");
        json.writeArrayFieldStart("children");
        for (LogRecord record : node.log()) {
            json.writeStartObject();
            json.writeStringField("message", (record.getLoggerName() == null ? "" : record.getLoggerName() + ": ") +
                                             (record.getMessage() != null ? record.getMessage() : "") +
                                             (record.getThrown() != null ? (record.getMessage() != null ? "\n" : "") + traceToString(record.getThrown()) : ""));
            json.writeNumberField("at", record.getInstant().toEpochMilli());
            json.writeStringField("level", typeOf(record.getLevel()));
            json.writeEndObject();
        }
        json.writeEndArray();
    }

    private static String traceToString(Throwable thrown) {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        thrown.printStackTrace(new PrintStream(buffer));
        return buffer.toString(UTF_8);
    }

    private interface Renderer {

        void render(OutputStream out) throws IOException;

    }

    private static class CustomJsonResponse extends HttpResponse {

        private final Renderer renderer;

        private CustomJsonResponse(Renderer renderer) {
            super(200);
            this.renderer = renderer;
        }

        @Override
        public void render(OutputStream outputStream) throws IOException {
            renderer.render(outputStream);
        }

        @Override
        public String getContentType() {
            return "application/json";
        }

        @Override
        public long maxPendingBytes() {
            return 1 << 25; // 32MB
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy