1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.testutil; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.content.Context; 22 import android.util.SparseArray; 23 import androidx.annotation.IntDef; 24 import androidx.annotation.Nullable; 25 import com.google.android.exoplayer2.C; 26 import com.google.android.exoplayer2.extractor.ExtractorOutput; 27 import com.google.android.exoplayer2.extractor.SeekMap; 28 import com.google.android.exoplayer2.util.Assertions; 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.IOException; 32 import java.io.PrintWriter; 33 import java.lang.annotation.Documented; 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 37 38 /** A fake {@link ExtractorOutput}. */ 39 public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpable { 40 41 private static final String DUMP_UPDATE_INSTRUCTIONS = 42 "To update the dump file, change FakeExtractorOutput#DUMP_FILE_ACTION to WRITE_TO_LOCAL (for" 43 + " Robolectric tests) or WRITE_TO_DEVICE (for instrumentation tests) and re-run the" 44 + " test."; 45 46 /** 47 * Possible actions to take with the dumps generated from this {@code FakeExtractorOutput} in 48 * {@link #assertOutput(Context, String)}. 49 */ 50 @Documented 51 @Retention(RetentionPolicy.SOURCE) 52 @IntDef( 53 flag = true, 54 value = {COMPARE_WITH_EXISTING, WRITE_TO_LOCAL, WRITE_TO_DEVICE}) 55 private @interface DumpFilesAction {} 56 /** Compare output with existing dump file. */ 57 private static final int COMPARE_WITH_EXISTING = 0; 58 /** 59 * Write output to the project folder {@code testdata/src/test/assets}. 60 * 61 * <p>Enabling this option works when tests are run in Android Studio. It may not work when the 62 * tests are run in another environment. 63 */ 64 private static final int WRITE_TO_LOCAL = 1; 65 /** Write output to folder {@code /storage/emulated/0/Android/data} of device. */ 66 private static final int WRITE_TO_DEVICE = 1 << 1; 67 68 @DumpFilesAction private static final int DUMP_FILE_ACTION = COMPARE_WITH_EXISTING; 69 70 public final SparseArray<FakeTrackOutput> trackOutputs; 71 72 public int numberOfTracks; 73 public boolean tracksEnded; 74 public @MonotonicNonNull SeekMap seekMap; 75 FakeExtractorOutput()76 public FakeExtractorOutput() { 77 trackOutputs = new SparseArray<>(); 78 } 79 80 @Override track(int id, int type)81 public FakeTrackOutput track(int id, int type) { 82 @Nullable FakeTrackOutput output = trackOutputs.get(id); 83 if (output == null) { 84 assertThat(tracksEnded).isFalse(); 85 numberOfTracks++; 86 output = new FakeTrackOutput(); 87 trackOutputs.put(id, output); 88 } 89 return output; 90 } 91 92 @Override endTracks()93 public void endTracks() { 94 tracksEnded = true; 95 } 96 97 @Override seekMap(SeekMap seekMap)98 public void seekMap(SeekMap seekMap) { 99 if (seekMap.isSeekable()) { 100 SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(0); 101 if (!seekPoints.first.equals(seekPoints.second)) { 102 throw new IllegalStateException("SeekMap defines two seek points for t=0"); 103 } 104 long durationUs = seekMap.getDurationUs(); 105 if (durationUs != C.TIME_UNSET) { 106 seekPoints = seekMap.getSeekPoints(durationUs); 107 if (!seekPoints.first.equals(seekPoints.second)) { 108 throw new IllegalStateException("SeekMap defines two seek points for t=durationUs"); 109 } 110 } 111 } 112 this.seekMap = seekMap; 113 } 114 clearTrackOutputs()115 public void clearTrackOutputs() { 116 for (int i = 0; i < numberOfTracks; i++) { 117 trackOutputs.valueAt(i).clear(); 118 } 119 } 120 121 /** 122 * Asserts that dump of this {@link FakeExtractorOutput} is equal to expected dump which is read 123 * from {@code dumpFile}. 124 * 125 * <p>If assertion fails because of an intended change in the output or a new dump file needs to 126 * be created, set {@link #DUMP_FILE_ACTION} to {@link #WRITE_TO_LOCAL} for local tests and to 127 * {@link #WRITE_TO_DEVICE} for instrumentation tests, and run the test again. Instead of 128 * assertion, actual dump will be written to {@code dumpFile}. For instrumentation tests, this new 129 * dump file needs to be copied to the project {@code testdata/src/test/assets} folder manually. 130 */ assertOutput(Context context, String dumpFile)131 public void assertOutput(Context context, String dumpFile) throws IOException { 132 String actual = new Dumper().add(this).toString(); 133 134 if (DUMP_FILE_ACTION == COMPARE_WITH_EXISTING) { 135 String expected; 136 try { 137 expected = TestUtil.getString(context, dumpFile); 138 } catch (FileNotFoundException e) { 139 throw new IOException("Dump file not found. " + DUMP_UPDATE_INSTRUCTIONS, e); 140 } 141 assertWithMessage( 142 "Extractor output doesn't match dump file: %s\n%s", 143 dumpFile, DUMP_UPDATE_INSTRUCTIONS) 144 .that(actual) 145 .isEqualTo(expected); 146 } else { 147 File file = 148 DUMP_FILE_ACTION == WRITE_TO_LOCAL 149 ? new File(System.getProperty("user.dir"), "../../testdata/src/test/assets") 150 : context.getExternalFilesDir(null); 151 file = new File(file, dumpFile); 152 Assertions.checkStateNotNull(file.getParentFile()).mkdirs(); 153 PrintWriter out = new PrintWriter(file); 154 out.print(actual); 155 out.close(); 156 } 157 } 158 159 @Override dump(Dumper dumper)160 public void dump(Dumper dumper) { 161 if (seekMap != null) { 162 dumper 163 .startBlock("seekMap") 164 .add("isSeekable", seekMap.isSeekable()) 165 .addTime("duration", seekMap.getDurationUs()) 166 .add("getPosition(0)", seekMap.getSeekPoints(0)); 167 if (seekMap.isSeekable()) { 168 dumper.add("getPosition(1)", seekMap.getSeekPoints(1)); 169 if (seekMap.getDurationUs() != C.TIME_UNSET) { 170 // Dump seek points at the mid point and duration. 171 long durationUs = seekMap.getDurationUs(); 172 long midPointUs = durationUs / 2; 173 dumper.add("getPosition(" + midPointUs + ")", seekMap.getSeekPoints(midPointUs)); 174 dumper.add("getPosition(" + durationUs + ")", seekMap.getSeekPoints(durationUs)); 175 } 176 } 177 dumper.endBlock(); 178 } 179 dumper.add("numberOfTracks", numberOfTracks); 180 for (int i = 0; i < numberOfTracks; i++) { 181 dumper.startBlock("track " + trackOutputs.keyAt(i)) 182 .add(trackOutputs.valueAt(i)) 183 .endBlock(); 184 } 185 dumper.add("tracksEnded", tracksEnded); 186 } 187 188 } 189