Back to Repositories

Testing File System Operations and Symlink Management in termux-app

A comprehensive unit test suite for validating file operations in the Termux app’s shared file utilities. This test class verifies core file system operations including directory creation, file manipulation, symlink handling, and error conditions.

Test Coverage Overview

The test suite provides extensive coverage of file system operations with a focus on directory and file manipulations.

Key areas tested include:
  • Directory creation, deletion, and validation
  • Regular file operations (create, read, write, copy)
  • Symlink handling (absolute and relative links)
  • Error handling for invalid operations
  • Empty directory validation with ignored paths

Implementation Analysis

The testing approach uses a systematic method to verify file operations in isolation and combination. The implementation employs Java’s File API with custom error handling through the FileUtils wrapper class.

Notable patterns include:
  • Hierarchical test structure with nested directories
  • Explicit verification of file states
  • Custom assertion methods for error checking
  • Comprehensive cleanup between test cases

Technical Details

Testing infrastructure includes:
  • Custom FileUtils wrapper class
  • Error handling through Errno system
  • Context-aware Android file operations
  • File path canonicalization
  • Character encoding support
  • Logging system for debugging

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Thorough setup and teardown of test environment
  • Explicit test labeling and error messages
  • Comprehensive validation of operation results
  • Edge case handling for symlinks and permissions
  • Modular test organization

termux/termux-app

termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java

            
package com.termux.shared.file.tests;

import android.content.Context;

import androidx.annotation.NonNull;

import com.termux.shared.errors.Errno;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.FileUtilsErrno;
import com.termux.shared.logger.Logger;
import com.termux.shared.errors.Error;

import java.io.File;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;

public class FileUtilsTests {

    private static final String LOG_TAG = "FileUtilsTests";

    /**
     * Run basic tests for {@link FileUtils} class.
     *
     * Move tests need to be written, specially for failures.
     *
     * The log level must be set to verbose.
     *
     * Run at app startup like in an activity
     * FileUtilsTests.runTests(this, TermuxConstants.TERMUX_HOME_DIR_PATH + "/FileUtilsTests");
     *
     * @param context The {@link Context} for operations.
     */
    public static void runTests(@NonNull final Context context, @NonNull final String testRootDirectoryPath) {
        try {
            Logger.logInfo(LOG_TAG, "Running tests");
            Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");

            String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null);
            assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);

            runTestsInner(testRootDirectoryPath);
            Logger.logInfo(LOG_TAG, "All tests successful");
        } catch (Exception e) {
            Logger.logErrorExtended(LOG_TAG, e.getMessage());
            Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true);
        }
    }

    private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception {
        Error error;
        String label;
        String path;

        /*
         * - dir1
         *  - sub_dir1
         *  - sub_reg1
         *  - sub_sym1 (absolute symlink to dir2)
         *  - sub_sym2 (copy of sub_sym1 for symlink to dir2)
         *  - sub_sym3 (relative symlink to dir4)
         * - dir2
         *  - sub_reg1
         *  - sub_reg2 (copy of dir2/sub_reg1)
         * - dir3 (copy of dir1)
         * - dir4 (moved from dir3)
         */

        String dir1_label = "dir1";
        String dir1_path = testRootDirectoryPath + "/dir1";

        String dir1__sub_dir1_label = "dir1/sub_dir1";
        String dir1__sub_dir1_path = dir1_path + "/sub_dir1";

        String dir1__sub_dir2_label = "dir1/sub_dir2";
        String dir1__sub_dir2_path = dir1_path + "/sub_dir2";

        String dir1__sub_dir3_label = "dir1/sub_dir3";
        String dir1__sub_dir3_path = dir1_path + "/sub_dir3";

        String dir1__sub_dir3__sub_reg1_label = "dir1/sub_dir3/sub_reg1";
        String dir1__sub_dir3__sub_reg1_path = dir1__sub_dir3_path + "/sub_reg1";

        String dir1__sub_reg1_label = "dir1/sub_reg1";
        String dir1__sub_reg1_path = dir1_path + "/sub_reg1";

        String dir1__sub_sym1_label = "dir1/sub_sym1";
        String dir1__sub_sym1_path = dir1_path + "/sub_sym1";

        String dir1__sub_sym2_label = "dir1/sub_sym2";
        String dir1__sub_sym2_path = dir1_path + "/sub_sym2";

        String dir1__sub_sym3_label = "dir1/sub_sym3";
        String dir1__sub_sym3_path = dir1_path + "/sub_sym3";


        String dir2_label = "dir2";
        String dir2_path = testRootDirectoryPath + "/dir2";

        String dir2__sub_reg1_label = "dir2/sub_reg1";
        String dir2__sub_reg1_path = dir2_path + "/sub_reg1";

        String dir2__sub_reg2_label = "dir2/sub_reg2";
        String dir2__sub_reg2_path = dir2_path + "/sub_reg2";


        String dir3_label = "dir3";
        String dir3_path = testRootDirectoryPath + "/dir3";

        String dir4_label = "dir4";
        String dir4_path = testRootDirectoryPath + "/dir4";





        // Create or clear test root directory file
        label = "testRootDirectoryPath";
        error = FileUtils.clearDirectory(label, testRootDirectoryPath);
        assertEqual("Failed to create " + label + " directory file", null, error);

        if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
            throwException("The " + label + " directory file does not exist as expected after creation");


        // Create dir1 directory file
        error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
        assertEqual("Failed to create " + dir1_label + " directory file", null, error);

        // Create dir2 directory file
        error = FileUtils.createDirectoryFile(dir2_label, dir2_path);
        assertEqual("Failed to create " + dir2_label + " directory file", null, error);





        // Create dir1/sub_dir1 directory file
        label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
        error = FileUtils.createDirectoryFile(label, path);
        assertEqual("Failed to create " + label + " directory file", null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after creation");

        // Create dir1/sub_reg1 regular file
        label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
        error = FileUtils.createRegularFile(label, path);
        assertEqual("Failed to create " + label + " regular file", null, error);
        if (!FileUtils.regularFileExists(path, false))
            throwException("The " + label + " regular file does not exist as expected after creation");

        // Create dir1/sub_sym1 -> dir2 absolute symlink file
        label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
        error = FileUtils.createSymlinkFile(label, dir2_path, path);
        assertEqual("Failed to create " + label + " symlink file", null, error);
        if (!FileUtils.symlinkFileExists(path))
            throwException("The " + label + " symlink file does not exist as expected after creation");

        // Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
        label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
        error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false);
        assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error);
        if (!FileUtils.symlinkFileExists(path))
            throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
        if (!new File(path).getCanonicalPath().equals(dir2_path))
            throwException("The " + label + " symlink file does not point to " + dir2_label);





        // Write "line1" to dir2/sub_reg1 regular file
        label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
        error = FileUtils.writeTextToFile(label, path, Charset.defaultCharset(), "line1", false);
        assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
        if (!FileUtils.regularFileExists(path, false))
            throwException("The " + label + " file does not exist as expected after writing to it with append mode false");

        // Write "line2" to dir2/sub_reg1 regular file
        error = FileUtils.writeTextToFile(label, path, Charset.defaultCharset(), "\nline2", true);
        assertEqual("Failed to write string to " + label + " file with append mode true", null, error);

        // Read dir2/sub_reg1 regular file
        StringBuilder dataStringBuilder = new StringBuilder();
        error = FileUtils.readTextFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false);
        assertEqual("Failed to read from " + label + " file", null, error);
        assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());

        // Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
        label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
        error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false);
        assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error);
        if (!FileUtils.regularFileExists(path, false))
            throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);





        // Copy dir1 directory file to dir3
        label = dir3_label; path = dir3_path;
        error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
        assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);

        // Copy dir1 directory file to dir3 again to test overwrite
        label = dir3_label; path = dir3_path;
        error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
        assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);

        // Move dir3 directory file to dir4
        label = dir4_label; path = dir4_path;
        error = FileUtils.moveDirectoryFile(label, dir3_path, path, false);
        assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);





        // Create dir1/sub_sym3 -> dir4 relative symlink file
        label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
        error = FileUtils.createSymlinkFile(label, "../dir4", path);
        assertEqual("Failed to create " + label + " symlink file", null, error);
        if (!FileUtils.symlinkFileExists(path))
            throwException("The " + label + " symlink file does not exist as expected after creation");

        // Create dir1/sub_sym3 -> dirX relative dangling symlink file
        // This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
        label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
        error = FileUtils.createSymlinkFile(label, "../dirX", path);
        assertEqual("Failed to create " + label + " symlink file", null, error);
        if (!FileUtils.symlinkFileExists(path))
            throwException("The " + label + " dangling symlink file does not exist as expected after creation");





        // Delete dir1/sub_sym2 symlink file
        label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
        error = FileUtils.deleteSymlinkFile(label, path, false);
        assertEqual("Failed to delete " + label + " symlink file", null, error);
        if (FileUtils.fileExists(path, false))
            throwException("The " + label + " symlink file still exist after deletion");

        // Check if dir2 directory file still exists after deletion of dir1/sub_sym2 since it was a symlink to dir2
        // When deleting a symlink file, its target must not be deleted
        label = dir2_label; path = dir2_path;
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1__sub_sym2_label);





        // Delete dir1 directory file
        label = dir1_label; path = dir1_path;
        error = FileUtils.deleteDirectoryFile(label, path, false);
        assertEqual("Failed to delete " + label + " directory file", null, error);
        if (FileUtils.fileExists(path, false))
            throwException("The " + label + " directory file still exist after deletion");


        // Check if dir2 directory file and dir2/sub_reg1 regular file still exist after deletion of
        // dir1 since there was a dir1/sub_sym1 symlink to dir2 in it
        // When deleting a directory, any targets of symlinks must not be deleted when deleting symlink files
        label = dir2_label; path = dir2_path;
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1_label);
        label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
        if (!FileUtils.fileExists(path, false))
            throwException("The " + label + " regular file has unexpectedly been deleted after deletion of " + dir1_label);





        // Delete dir2/sub_reg1 regular file
        label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
        error = FileUtils.deleteRegularFile(label, path, false);
        assertEqual("Failed to delete " + label + " regular file", null, error);
        if (FileUtils.fileExists(path, false))
            throwException("The " + label + " regular file still exist after deletion");


        List<String> ignoredSubFilePaths = Arrays.asList(dir1__sub_dir2_path, dir1__sub_dir3__sub_reg1_path);

        // Create dir1 directory file
        error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
        assertEqual("Failed to create " + dir1_label + " directory file", null, error);

        // Test empty dir
        error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
        assertEqual("Failed to validate if " + dir1_label + " directory file is empty", null, error);


        // Create dir1/sub_dir3 directory file
        label = dir1__sub_dir3_label; path = dir1__sub_dir3_path;
        error = FileUtils.createDirectoryFile(label, path);
        assertEqual("Failed to create " + label + " directory file", null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after creation");

        // Test parent dir existing of non existing ignored regular file
        error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
        assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with parent dir existing of non existing ignored regular file", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error);


        // Write "line1" to dir1/sub_dir3/sub_reg1 regular file
        label = dir1__sub_dir3__sub_reg1_label; path = dir1__sub_dir3__sub_reg1_path;
        error = FileUtils.writeTextToFile(label, path, Charset.defaultCharset(), "line1", false);
        assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
        if (!FileUtils.regularFileExists(path, false))
            throwException("The " + label + " file does not exist as expected after writing to it with append mode false");

        // Test ignored regular file existing
        error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
        assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored regular file existing", null, error);


        // Create dir1/sub_dir2 directory file
        label = dir1__sub_dir2_label; path = dir1__sub_dir2_path;
        error = FileUtils.createDirectoryFile(label, path);
        assertEqual("Failed to create " + label + " directory file", null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after creation");

        // Test ignored dir file existing
        error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
        assertEqual("Failed to validate if " + dir1_label + " directory file is empty with ignored dir file existing", null, error);


        // Create dir1/sub_dir1 directory file
        label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
        error = FileUtils.createDirectoryFile(label, path);
        assertEqual("Failed to create " + label + " directory file", null, error);
        if (!FileUtils.directoryFileExists(path, false))
            throwException("The " + label + " directory file does not exist as expected after creation");

        // Test non ignored dir file existing
        error = FileUtils.validateDirectoryFileEmptyOrOnlyContainsSpecificFiles(dir1_label, dir1_path, ignoredSubFilePaths, false);
        assertErrnoEqual("Failed to validate if " + dir1_label + " directory file is empty with non ignored dir file existing", FileUtilsErrno.ERRNO_NON_EMPTY_DIRECTORY_FILE, error);


        // Delete dir1 directory file
        label = dir1_label; path = dir1_path;
        error = FileUtils.deleteDirectoryFile(label, path, false);
        assertEqual("Failed to delete " + label + " directory file", null, error);


        FileUtils.getFileType("/dev/ptmx", false);
        FileUtils.getFileType("/dev/null", false);
    }



    public static void assertEqual(@NonNull final String message, final String expected, final Error actual) throws Exception {
        String actualString = actual != null ? actual.getMessage() : null;
        if (!equalsRegardingNull(expected, actualString))
            throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
    }

    public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
        if (!equalsRegardingNull(expected, actual))
            throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");
    }

    private static boolean equalsRegardingNull(final String expected, final String actual) {
        if (expected == null) {
            return actual == null;
        }

        return isEquals(expected, actual);
    }

    public static void assertErrnoEqual(@NonNull final String message, final Errno expected, final Error actual) throws Exception {
        if ((expected == null && actual != null) || (expected != null && !expected.equalsErrorTypeAndCode(actual)))
            throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
    }



    private static boolean isEquals(String expected, String actual) {
        return expected.equals(actual);
    }

    public static void throwException(@NonNull final String message) throws Exception {
            throw new Exception(message);
    }

}