/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tradefed.util;

import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * A utility class that resets and forces a flush of Java code coverage measurements from processes
 * running on the device.
 */
public class JavaCodeCoverageFlusher {

    private static final String COVERAGE_RESET_FORMAT =
            "am attach-agent %s /system/lib/libdumpcoverage.so=reset";
    private static final String COVERAGE_FLUSH_FORMAT =
            "am attach-agent %s /system/lib/libdumpcoverage.so=dump:%s";
    private static final String PACKAGE_PREFIX = "package:";
    private static final long FLUSH_DELAY = 60 * 1000; // 1 minute

    private IRunUtil mRunUtil = RunUtil.getDefault();

    private final ITestDevice mDevice;
    private final List<String> mProcessNames;

    public JavaCodeCoverageFlusher(ITestDevice device, List<String> processNames) {
        mDevice = device;
        mProcessNames = ImmutableList.copyOf(processNames);
    }

    @VisibleForTesting
    void setRunUtil(IRunUtil runUtil) {
        mRunUtil = runUtil;
    }

    /** Retrieves the name of all running processes on the device. */
    private List<String> getAllProcessNames() throws DeviceNotAvailableException {
        List<ProcessInfo> processes = PsParser.getProcesses(mDevice.executeShellCommand("ps -e"));
        List<String> names = new ArrayList<>();

        for (ProcessInfo pinfo : processes) {
            names.add(pinfo.getName());
        }

        return names;
    }

    /** Retrieves the set of all installed packages on the device. */
    private Set<String> getAllPackages() throws DeviceNotAvailableException {
        Splitter splitter = Splitter.on('\n').trimResults().omitEmptyStrings();
        String pmListPackages = mDevice.executeShellCommand("pm list packages -a");

        List<String> lines = splitter.splitToList(pmListPackages);
        ImmutableSet.Builder<String> packages = ImmutableSet.builder();

        // Strip "package:" prefix from the output.
        for (String line : lines) {
            if (line.startsWith(PACKAGE_PREFIX)) {
                line = line.substring(PACKAGE_PREFIX.length());
            }

            packages.add(line);
        }

        return packages.build();
    }

    /**
     * Resets the Java code coverage counters for the given processes.
     *
     * @throws DeviceNotAvailableException
     */
    public void resetCoverage() throws DeviceNotAvailableException {
        List<String> processNames = mProcessNames;

        if (processNames.isEmpty()) {
            processNames = getAllProcessNames();
        }

        Set<String> javaPackages = getAllPackages();

        // Attempt to reset the coverage measurements for the given processes.
        for (String processName :
                Sets.intersection(javaPackages, ImmutableSet.copyOf(processNames))) {
            String pid = mDevice.getProcessPid(processName);

            if (pid != null) {
                mDevice.executeShellCommand(String.format(COVERAGE_RESET_FORMAT, processName));
            }
        }

        // Reset coverage for system_server.
        mDevice.executeShellCommand("cmd coverage reset");
    }

    /**
     * Forces a flush of Java coverage data from processes running on the device.
     *
     * @return a list of the coverage files generated on the device.
     * @throws DeviceNotAvailableException
     */
    public List<String> forceCoverageFlush() throws DeviceNotAvailableException {
        List<String> processNames = mProcessNames;
        if (processNames.isEmpty()) {
            processNames = getAllProcessNames();
        }

        List<String> coverageFiles = new ArrayList<>();

        Set<String> javaPackages = getAllPackages();

        // Flush coverage for the given processes, if the process exists and is a Java process.
        for (String processName :
                Sets.intersection(javaPackages, ImmutableSet.copyOf(processNames))) {
            String pid = mDevice.getProcessPid(processName);

            if (pid != null) {
                String outputPath = String.format("/data/misc/trace/%s-%s.ec", processName, pid);
                mDevice.executeShellCommand(
                        String.format(COVERAGE_FLUSH_FORMAT, processName, outputPath));
                coverageFiles.add(outputPath);
            }
        }

        // Flush coverage for system_server.
        mDevice.executeShellCommand("cmd coverage dump /data/misc/trace/system_server.ec");
        coverageFiles.add("/data/misc/trace/system_server.ec");

        // Allow time for the system to fully write coverage to disk before pulling measurements.
        // This reduces the rate of incomplete coverage measurements received.
        mRunUtil.sleep(FLUSH_DELAY);

        return coverageFiles;
    }
}
