1 /* 2 * Copyright 2013 Google Inc. 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 17 package com.google.common.jimfs; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.truth.Fact.fact; 21 import static com.google.common.truth.Fact.simpleFact; 22 import static java.nio.charset.StandardCharsets.UTF_8; 23 import static java.util.Arrays.asList; 24 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.io.BaseEncoding; 27 import com.google.common.truth.FailureMetadata; 28 import com.google.common.truth.Subject; 29 import java.io.IOException; 30 import java.nio.charset.Charset; 31 import java.nio.file.DirectoryStream; 32 import java.nio.file.Files; 33 import java.nio.file.LinkOption; 34 import java.nio.file.Path; 35 import java.nio.file.PathMatcher; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.List; 39 import org.checkerframework.checker.nullness.compatqual.NullableDecl; 40 41 /** 42 * Subject for doing assertions on file system paths. 43 * 44 * @author Colin Decker 45 */ 46 public final class PathSubject extends Subject { 47 48 /** Returns the subject factory for doing assertions on paths. */ paths()49 public static Subject.Factory<PathSubject, Path> paths() { 50 return new PathSubjectFactory(); 51 } 52 53 private static final LinkOption[] FOLLOW_LINKS = new LinkOption[0]; 54 private static final LinkOption[] NOFOLLOW_LINKS = {LinkOption.NOFOLLOW_LINKS}; 55 56 private final Path actual; 57 protected LinkOption[] linkOptions = FOLLOW_LINKS; 58 private Charset charset = UTF_8; 59 PathSubject(FailureMetadata failureMetadata, Path subject)60 private PathSubject(FailureMetadata failureMetadata, Path subject) { 61 super(failureMetadata, subject); 62 this.actual = subject; 63 } 64 toPath(String path)65 private Path toPath(String path) { 66 return actual.getFileSystem().getPath(path); 67 } 68 69 /** Returns this, for readability of chained assertions. */ and()70 public PathSubject and() { 71 return this; 72 } 73 74 /** Do not follow links when looking up the path. */ noFollowLinks()75 public PathSubject noFollowLinks() { 76 this.linkOptions = NOFOLLOW_LINKS; 77 return this; 78 } 79 80 /** 81 * Set the given charset to be used when reading the file at this path as text. Default charset if 82 * not set is UTF-8. 83 */ withCharset(Charset charset)84 public PathSubject withCharset(Charset charset) { 85 this.charset = checkNotNull(charset); 86 return this; 87 } 88 89 /** Asserts that the path is absolute (it has a root component). */ isAbsolute()90 public PathSubject isAbsolute() { 91 if (!actual.isAbsolute()) { 92 failWithActual(simpleFact("expected to be absolute")); 93 } 94 return this; 95 } 96 97 /** Asserts that the path is relative (it has no root component). */ isRelative()98 public PathSubject isRelative() { 99 if (actual.isAbsolute()) { 100 failWithActual(simpleFact("expected to be relative")); 101 } 102 return this; 103 } 104 105 /** Asserts that the path has the given root component. */ hasRootComponent(@ullableDecl String root)106 public PathSubject hasRootComponent(@NullableDecl String root) { 107 Path rootComponent = actual.getRoot(); 108 if (root == null && rootComponent != null) { 109 failWithActual("expected to have root component", root); 110 } else if (root != null && !root.equals(rootComponent.toString())) { 111 failWithActual("expected to have root component", root); 112 } 113 return this; 114 } 115 116 /** Asserts that the path has no name components. */ hasNoNameComponents()117 public PathSubject hasNoNameComponents() { 118 check("getNameCount()").that(actual.getNameCount()).isEqualTo(0); 119 return this; 120 } 121 122 /** Asserts that the path has the given name components. */ hasNameComponents(String... names)123 public PathSubject hasNameComponents(String... names) { 124 ImmutableList.Builder<String> builder = ImmutableList.builder(); 125 for (Path name : actual) { 126 builder.add(name.toString()); 127 } 128 129 if (!builder.build().equals(ImmutableList.copyOf(names))) { 130 failWithActual("expected components", asList(names)); 131 } 132 return this; 133 } 134 135 /** Asserts that the path matches the given syntax and pattern. */ matches(String syntaxAndPattern)136 public PathSubject matches(String syntaxAndPattern) { 137 PathMatcher matcher = actual.getFileSystem().getPathMatcher(syntaxAndPattern); 138 if (!matcher.matches(actual)) { 139 failWithActual("expected to match ", syntaxAndPattern); 140 } 141 return this; 142 } 143 144 /** Asserts that the path does not match the given syntax and pattern. */ doesNotMatch(String syntaxAndPattern)145 public PathSubject doesNotMatch(String syntaxAndPattern) { 146 PathMatcher matcher = actual.getFileSystem().getPathMatcher(syntaxAndPattern); 147 if (matcher.matches(actual)) { 148 failWithActual("expected not to match", syntaxAndPattern); 149 } 150 return this; 151 } 152 153 /** Asserts that the path exists. */ exists()154 public PathSubject exists() { 155 if (!Files.exists(actual, linkOptions)) { 156 failWithActual(simpleFact("expected to exist")); 157 } 158 if (Files.notExists(actual, linkOptions)) { 159 failWithActual(simpleFact("expected to exist")); 160 } 161 return this; 162 } 163 164 /** Asserts that the path does not exist. */ doesNotExist()165 public PathSubject doesNotExist() { 166 if (!Files.notExists(actual, linkOptions)) { 167 failWithActual(simpleFact("expected not to exist")); 168 } 169 if (Files.exists(actual, linkOptions)) { 170 failWithActual(simpleFact("expected not to exist")); 171 } 172 return this; 173 } 174 175 /** Asserts that the path is a directory. */ isDirectory()176 public PathSubject isDirectory() { 177 exists(); // check for directoryness should imply check for existence 178 179 if (!Files.isDirectory(actual, linkOptions)) { 180 failWithActual(simpleFact("expected to be directory")); 181 } 182 return this; 183 } 184 185 /** Asserts that the path is a regular file. */ isRegularFile()186 public PathSubject isRegularFile() { 187 exists(); // check for regular fileness should imply check for existence 188 189 if (!Files.isRegularFile(actual, linkOptions)) { 190 failWithActual(simpleFact("expected to be regular file")); 191 } 192 return this; 193 } 194 195 /** Asserts that the path is a symbolic link. */ isSymbolicLink()196 public PathSubject isSymbolicLink() { 197 exists(); // check for symbolic linkness should imply check for existence 198 199 if (!Files.isSymbolicLink(actual)) { 200 failWithActual(simpleFact("expected to be symbolic link")); 201 } 202 return this; 203 } 204 205 /** Asserts that the path, which is a symbolic link, has the given path as a target. */ withTarget(String targetPath)206 public PathSubject withTarget(String targetPath) throws IOException { 207 Path actualTarget = Files.readSymbolicLink(actual); 208 if (!actualTarget.equals(toPath(targetPath))) { 209 failWithoutActual( 210 fact("expected link target", targetPath), 211 fact("but target was", actualTarget), 212 fact("for path", actual)); 213 } 214 return this; 215 } 216 217 /** 218 * Asserts that the file the path points to exists and has the given number of links to it. Fails 219 * on a file system that does not support the "unix" view. 220 */ hasLinkCount(int count)221 public PathSubject hasLinkCount(int count) throws IOException { 222 exists(); 223 224 int linkCount = (int) Files.getAttribute(actual, "unix:nlink", linkOptions); 225 if (linkCount != count) { 226 failWithActual("expected to have link count", count); 227 } 228 return this; 229 } 230 231 /** Asserts that the path resolves to the same file as the given path. */ isSameFileAs(String path)232 public PathSubject isSameFileAs(String path) throws IOException { 233 return isSameFileAs(toPath(path)); 234 } 235 236 /** Asserts that the path resolves to the same file as the given path. */ isSameFileAs(Path path)237 public PathSubject isSameFileAs(Path path) throws IOException { 238 if (!Files.isSameFile(actual, path)) { 239 failWithActual("expected to be same file as", path); 240 } 241 return this; 242 } 243 244 /** Asserts that the path does not resolve to the same file as the given path. */ isNotSameFileAs(String path)245 public PathSubject isNotSameFileAs(String path) throws IOException { 246 if (Files.isSameFile(actual, toPath(path))) { 247 failWithActual("expected not to be same file as", path); 248 } 249 return this; 250 } 251 252 /** Asserts that the directory has no children. */ hasNoChildren()253 public PathSubject hasNoChildren() throws IOException { 254 isDirectory(); 255 256 try (DirectoryStream<Path> stream = Files.newDirectoryStream(actual)) { 257 if (stream.iterator().hasNext()) { 258 failWithActual(simpleFact("expected to have no children")); 259 } 260 } 261 return this; 262 } 263 264 /** Asserts that the directory has children with the given names, in the given order. */ hasChildren(String... children)265 public PathSubject hasChildren(String... children) throws IOException { 266 isDirectory(); 267 268 List<Path> expectedNames = new ArrayList<>(); 269 for (String child : children) { 270 expectedNames.add(actual.getFileSystem().getPath(child)); 271 } 272 273 try (DirectoryStream<Path> stream = Files.newDirectoryStream(actual)) { 274 List<Path> actualNames = new ArrayList<>(); 275 for (Path path : stream) { 276 actualNames.add(path.getFileName()); 277 } 278 279 if (!actualNames.equals(expectedNames)) { 280 failWithoutActual( 281 fact("expected to have children", expectedNames), 282 fact("but had children", actualNames), 283 fact("for path", actual)); 284 } 285 } 286 return this; 287 } 288 289 /** Asserts that the file has the given size. */ hasSize(long size)290 public PathSubject hasSize(long size) throws IOException { 291 if (Files.size(actual) != size) { 292 failWithActual("expected to have size", size); 293 } 294 return this; 295 } 296 297 /** Asserts that the file is a regular file containing no bytes. */ containsNoBytes()298 public PathSubject containsNoBytes() throws IOException { 299 return containsBytes(new byte[0]); 300 } 301 302 /** 303 * Asserts that the file is a regular file containing exactly the byte values of the given ints. 304 */ containsBytes(int... bytes)305 public PathSubject containsBytes(int... bytes) throws IOException { 306 byte[] realBytes = new byte[bytes.length]; 307 for (int i = 0; i < bytes.length; i++) { 308 realBytes[i] = (byte) bytes[i]; 309 } 310 return containsBytes(realBytes); 311 } 312 313 /** Asserts that the file is a regular file containing exactly the given bytes. */ containsBytes(byte[] bytes)314 public PathSubject containsBytes(byte[] bytes) throws IOException { 315 isRegularFile(); 316 hasSize(bytes.length); 317 318 byte[] actual = Files.readAllBytes(this.actual); 319 if (!Arrays.equals(bytes, actual)) { 320 System.out.println(BaseEncoding.base16().encode(actual)); 321 System.out.println(BaseEncoding.base16().encode(bytes)); 322 failWithActual("expected to contain bytes", BaseEncoding.base16().encode(bytes)); 323 } 324 return this; 325 } 326 327 /** 328 * Asserts that the file is a regular file containing the same bytes as the regular file at the 329 * given path. 330 */ containsSameBytesAs(String path)331 public PathSubject containsSameBytesAs(String path) throws IOException { 332 isRegularFile(); 333 334 byte[] expectedBytes = Files.readAllBytes(toPath(path)); 335 if (!Arrays.equals(expectedBytes, Files.readAllBytes(actual))) { 336 failWithActual("expected to contain same bytes as", path); 337 } 338 return this; 339 } 340 341 /** 342 * Asserts that the file is a regular file containing the given lines of text. By default, the 343 * bytes are decoded as UTF-8; for a different charset, use {@link #withCharset(Charset)}. 344 */ containsLines(String... lines)345 public PathSubject containsLines(String... lines) throws IOException { 346 return containsLines(Arrays.asList(lines)); 347 } 348 349 /** 350 * Asserts that the file is a regular file containing the given lines of text. By default, the 351 * bytes are decoded as UTF-8; for a different charset, use {@link #withCharset(Charset)}. 352 */ containsLines(Iterable<String> lines)353 public PathSubject containsLines(Iterable<String> lines) throws IOException { 354 isRegularFile(); 355 356 List<String> expected = ImmutableList.copyOf(lines); 357 List<String> actual = Files.readAllLines(this.actual, charset); 358 check("lines()").that(actual).isEqualTo(expected); 359 return this; 360 } 361 362 /** Returns an object for making assertions about the given attribute. */ attribute(final String attribute)363 public Attribute attribute(final String attribute) { 364 return new Attribute() { 365 @Override 366 public Attribute is(Object value) throws IOException { 367 Object actualValue = Files.getAttribute(actual, attribute, linkOptions); 368 check("attribute(%s)", attribute).that(actualValue).isEqualTo(value); 369 return this; 370 } 371 372 @Override 373 public Attribute isNot(Object value) throws IOException { 374 Object actualValue = Files.getAttribute(actual, attribute, linkOptions); 375 check("attribute(%s)", attribute).that(actualValue).isNotEqualTo(value); 376 return this; 377 } 378 379 @Override 380 public PathSubject and() { 381 return PathSubject.this; 382 } 383 }; 384 } 385 386 private static class PathSubjectFactory implements Subject.Factory<PathSubject, Path> { 387 388 @Override 389 public PathSubject createSubject(FailureMetadata failureMetadata, Path that) { 390 return new PathSubject(failureMetadata, that); 391 } 392 } 393 394 /** Interface for assertions about a file attribute. */ 395 public interface Attribute { 396 397 /** Asserts that the value of this attribute is equal to the given value. */ 398 Attribute is(Object value) throws IOException; 399 400 /** Asserts that the value of this attribute is not equal to the given value. */ 401 Attribute isNot(Object value) throws IOException; 402 403 /** Returns the path subject for further chaining. */ 404 PathSubject and(); 405 } 406 } 407