• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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