package org.jetbrains.jps.android; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; import org.jetbrains.android.util.AndroidBuildTestingManager; import org.jetbrains.annotations.NotNull; import java.io.*; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Eugene.Kudelevsky */ abstract class AndroidBuildTestingCommandExecutor implements AndroidBuildTestingManager.MyCommandExecutor { public static final String ENTRY_HEADER = "______ENTRY_"; private volatile StringWriter myStringWriter = new StringWriter(); private final List<Pair<String, Pattern>> myPathPatterns = new ArrayList<Pair<String, Pattern>>(); private final Set<String> myCheckedJars = new HashSet<String>(); public void addPathPrefix(@NotNull String id, @NotNull String prefix) { myPathPatterns.add(Pair.create(id, Pattern.compile("(" + FileUtil.toSystemIndependentName(prefix) + ").*"))); } public void addRegexPathPattern(@NotNull String id, @NotNull String regex) { myPathPatterns.add(Pair.create(id, Pattern.compile("(" + regex + ")"))); } public void addRegexPathPatternPrefix(@NotNull String id, @NotNull String regex) { myPathPatterns.add(Pair.create(id, Pattern.compile("(" + regex + ").*"))); } @NotNull @Override public Process createProcess(@NotNull String[] args, @NotNull Map<? extends String, ? extends String> environment) { startNewEntry(); final String[] argsToLog = processArgs(args); logString(StringUtil.join(argsToLog, "\n")); if (environment.size() > 0) { final StringBuilder envBuilder = new StringBuilder(); for (Map.Entry<? extends String, ? extends String> entry : environment.entrySet()) { if (envBuilder.length() > 0) { envBuilder.append(", "); } final String value = progessArg(entry.getValue()); envBuilder.append(entry.getKey()).append("=").append(value); } logString("\nenv: " + envBuilder.toString()); } logString("\n\n"); try { return doCreateProcess(args, environment); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } @Override public void log(@NotNull String s) { startNewEntry(); final String[] args = s.split("\\n"); logString(StringUtil.join(processArgs(args), "\n")); logString("\n\n"); } @Override public void checkJarContent(@NotNull String jarId, @NotNull String jarPath) { doCheckJar(jarId, jarPath); myCheckedJars.add(jarId); } protected void doCheckJar(@NotNull String jarId, @NotNull String jarPath) { } private synchronized void startNewEntry() { logString(ENTRY_HEADER + "\n"); } private synchronized void logString(String s) { myStringWriter.write(s); } private String[] processArgs(String[] args) { final String[] result = new String[args.length]; for (int i = 0; i < result.length; i++) { result[i] = progessArg(args[i]); } return result; } private String progessArg(String arg) { final String[] subargs = arg.split(File.pathSeparator); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < subargs.length; i++) { String subarg = subargs[i]; String s = FileUtil.toSystemIndependentName(subarg); if (s.endsWith(".exe")) { s = FileUtilRt.getNameWithoutExtension(s); } for (Pair<String, Pattern> pair : myPathPatterns) { final String id = pair.getFirst(); final Pattern prefixPattern = pair.getSecond(); final Matcher matcher = prefixPattern.matcher(s); if (matcher.matches()) { s = "$" + id + "$" + s.substring(matcher.group(1).length()); } } builder.append(s); if (i < subargs.length - 1) { builder.append(File.pathSeparator); } } return builder.toString(); } @NotNull protected abstract Process doCreateProcess(@NotNull String[] args, @NotNull Map<? extends String, ? extends String> environment) throws Exception; @NotNull public synchronized String getLog() { return myStringWriter.toString(); } public synchronized void clear() { myStringWriter = new StringWriter(); myCheckedJars.clear(); } @NotNull protected Set<String> getCheckedJars() { return myCheckedJars; } public static String normalizeExpectedLog(@NotNull String expectedLog, @NotNull String actualLog) { final String[] actualEntries = actualLog.split(AndroidBuildTestingCommandExecutor.ENTRY_HEADER); final List<String> actualEntryList = new ArrayList<String>(); for (String entry : actualEntries) { final String s = entry.trim(); if (s.length() == 0) { continue; } actualEntryList.add(s); } final Map<String, MyLogEntry> id2logEntry = new HashMap<String, MyLogEntry>(); final String[] entries = expectedLog.split(AndroidBuildTestingCommandExecutor.ENTRY_HEADER); for (String entry : entries) { final String s = entry.trim(); if (s.length() == 0) { continue; } final int newLineIdx = s.indexOf('\n'); final int colonIdx = s.indexOf(':'); assert colonIdx >= 0 && colonIdx < newLineIdx; final String id = s.substring(0, colonIdx); final String depIdsStr = s.substring(colonIdx + 1, newLineIdx); final String[] depIds = depIdsStr.length() > 0 ? depIdsStr.trim().split(",") : ArrayUtil.EMPTY_STRING_ARRAY; final String content = s.substring(newLineIdx + 1).trim(); id2logEntry.put(id, new MyLogEntry(content, depIds)); } final List<String> addedEntries = new ArrayList<String>(); final Set<String> addedEntriesSet = new HashSet<String>(); while (addedEntries.size() < id2logEntry.size()) { final List<String> candidates = new ArrayList<String>(); for (Map.Entry<String, MyLogEntry> entry : id2logEntry.entrySet()) { final String id = entry.getKey(); if (addedEntriesSet.contains(id)) { continue; } final MyLogEntry logEntry = entry.getValue(); boolean canBeAdded = true; for (String depId : logEntry.myDepIds) { if (!addedEntriesSet.contains(depId)) { canBeAdded = false; break; } } if (canBeAdded) { candidates.add(id); } } if (candidates.size() == 0) { throw new RuntimeException("The log graph contains cycles"); } boolean added = false; if (actualEntryList.size() > addedEntries.size()) { final String actualEntryContent = actualEntryList.get(addedEntries.size()); for (String id : candidates) { if (id2logEntry.get(id).myContent.equals(actualEntryContent)) { addedEntries.add(id); addedEntriesSet.add(id); added = true; break; } } } if (!added) { addedEntriesSet.addAll(candidates); addedEntries.addAll(candidates); } } final StringBuilder builder = new StringBuilder(); for (String id : addedEntries) { final MyLogEntry entry = id2logEntry.get(id); builder.append(entry.myContent).append("\n\n"); } return builder.toString(); } public static String normalizeLog(@NotNull String log) { final String[] entries = log.split(AndroidBuildTestingCommandExecutor.ENTRY_HEADER); final StringBuilder result = new StringBuilder(); for (String entry : entries) { final String s = entry.trim(); if (s.length() == 0) { continue; } result.append(s).append("\n\n"); } return result.toString(); } protected static class MyProcess extends Process { private final int myExitValue; private final String myOutputText; private final String myErrorText; protected MyProcess(int exitValue, @NotNull String outputText, @NotNull String errorText) { myExitValue = exitValue; myOutputText = outputText; myErrorText = errorText; } @Override public OutputStream getOutputStream() { throw new UnsupportedOperationException(); } @Override public InputStream getInputStream() { return stringToInputStream(myOutputText); } @Override public InputStream getErrorStream() { return stringToInputStream(myErrorText); } private static ByteArrayInputStream stringToInputStream(String s) { return new ByteArrayInputStream(s.getBytes(Charset.defaultCharset())); } @Override public int waitFor() throws InterruptedException { return exitValue(); } @Override public int exitValue() { return myExitValue; } @Override public void destroy() { } } private static class MyLogEntry { final String myContent; final String[] myDepIds; private MyLogEntry(@NotNull String content, @NotNull String[] depIds) { myContent = content; myDepIds = depIds; } } }