]> _ Git - typetest.git/commitdiff
Initial commit master
authormivirl <>
Wed, 22 May 2024 00:20:30 +0000 (19:20 -0500)
committermivirl <>
Wed, 22 May 2024 00:20:30 +0000 (19:20 -0500)
README.md [new file with mode: 0644]
makefile [new file with mode: 0644]
src/log.h [new file with mode: 0644]
src/test/ex1.txt [new file with mode: 0644]
src/test/ex2.txt [new file with mode: 0644]
src/test/ex3.txt [new file with mode: 0644]
src/test/test.sh [new file with mode: 0755]
src/typetest.c [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..3128b32
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# Typing test
+
+A simple typing test program that reads input from either a file or stdin.
+Prints the input file one line at a time without showing feedback until a
+line has been submitted by the user. Indicates mistakes after the whole line
+has been entered.
+
+Ignores leading whitespace, either from the file or entered by the user.
+
+## Build & run
+
+```sh
+make
+./build/typetest /path/to/file
+```
diff --git a/makefile b/makefile
new file mode 100644 (file)
index 0000000..ddb07dc
--- /dev/null
+++ b/makefile
@@ -0,0 +1,69 @@
+# Makefile based on https://spin.atomicobject.com/makefile-c-projects/
+TARGET_EXEC ?= typetest
+
+BUILD_DIR ?= ./build
+SRC_DIRS ?= ./src
+
+SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
+OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
+DEPS := $(OBJS:.o=.d)
+
+INC_DIRS := $(shell find $(SRC_DIRS) -type d)
+INC_FLAGS := $(addprefix -I,$(INC_DIRS))
+
+# Flags from https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html
+CPPFLAGS ?= $(INC_FLAGS) -MMD -MP \
+       -O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \
+       -Werror=format-security \
+       -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
+       -D_GLIBCXX_ASSERTIONS \
+       -fstack-clash-protection -fstack-protector-strong \
+       -Wl,-z,nodlopen -Wl,-z,noexecstack \
+       -Wl,-z,relro -Wl,-z,now \
+       -fPIE -pie \
+       -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing \
+       -Werror=implicit -Werror=incompatible-pointer-types -Werror=int-conversion \
+       -fexceptions
+
+$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
+       $(CC) $(CPPFLAGS) $(OBJS) -o $@ $(LDFLAGS)
+
+# Assembly
+$(BUILD_DIR)/%.s.o: %.s
+       mkdir -p $(dir $@)
+       $(AS) $(ASFLAGS) -c $< -o $@
+
+# C source
+$(BUILD_DIR)/%.c.o: %.c
+       mkdir -p $(dir $@)
+       $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+
+# C++ source
+$(BUILD_DIR)/%.cpp.o: %.cpp
+       mkdir -p $(dir $@)
+       $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
+
+.PHONY: debug release tidy format clangdb clean test
+
+debug: CPPFLAGS += -g -fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=leak -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=bounds-strict -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow
+debug: $(BUILD_DIR)/$(TARGET_EXEC)
+
+release: CPPFLAGS += -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero
+release: $(BUILD_DIR)/$(TARGET_EXEC)
+
+clangdb: clean
+       bear -- make
+
+tidy:
+       clang-tidy $(SRCS)
+
+format:
+       clang-format --style=WebKit $(SRCS)
+
+clean:
+       $(RM) -r $(BUILD_DIR)
+
+test: $(BUILD_DIR)/$(TARGET_EXEC)
+       ./src/test/test.sh
+
+-include $(DEPS)
diff --git a/src/log.h b/src/log.h
new file mode 100644 (file)
index 0000000..e0860a0
--- /dev/null
+++ b/src/log.h
@@ -0,0 +1,61 @@
+#ifndef LOG_H
+#define LOG_H
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+
+/* Logging macros
+Usage:
+- Define int LOGLVL
+- Use logging macros for corresponding level with printf format string
+- Use assert macros to test conditions and log/handle errors
+*/
+
+/* Use to not jump to the error label when using ASSERT:
+     #define GOTO_ERROR
+*/
+
+extern int LOGLVL;
+#define LOGLVL_ERROR 0
+#define LOGLVL_WARN 1
+#define LOGLVL_INFO 2
+#define LOGLVL_DEBUG 3
+
+#define ERRNOMSG(x) (errno ? strerror(errno) : "n/a")
+
+#define LOG_ERROR(...) if (LOGLVL >= LOGLVL_ERROR) {\
+    fprintf (stderr, "[ERROR]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+            __func__, ERRNOMSG(errno));\
+    fprintf (stderr, __VA_ARGS__);\
+    fprintf (stderr, "\n"); }
+
+#define LOG_WARN(...) if (LOGLVL >= LOGLVL_WARN) {\
+    fprintf (stderr, "[WARN]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+            __func__, ERRNOMSG(errno));\
+    fprintf (stderr, __VA_ARGS__);\
+    fprintf (stderr, "\n"); }
+
+#define LOG_INFO(...) if (LOGLVL >= LOGLVL_INFO) {\
+    fprintf (stderr, "[INFO]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+            __func__, ERRNOMSG(errno));\
+    fprintf (stderr, __VA_ARGS__);\
+    fprintf (stderr, "\n"); }
+
+#define LOG_DEBUG(...) if (LOGLVL >= LOGLVL_DEBUG) {\
+    fprintf (stderr, "[DEBUG]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+            __func__, ERRNOMSG(errno));\
+    fprintf (stderr, __VA_ARGS__);\
+    fprintf (stderr, "\n"); }
+
+#ifndef GOTO_ERROR
+#define GOTO_ERROR goto error
+#endif
+
+#define ASSERT(x, ...) if (!(x)) { LOG_ERROR(__VA_ARGS__); errno = 0; GOTO_ERROR;}
+#define ASSERT_WARN(x, ...) if (!(x)) { LOG_WARN(__VA_ARGS__); errno = 0; GOTO_ERROR;}
+#define ASSERT_INFO(x, ...) if (!(x)) { LOG_INFO(__VA_ARGS__); errno = 0; }
+#define ASSERT_DEBUG(x, ...) if (!(x)) { LOG_DEBUG(__VA_ARGS__); errno = 0; }
+
+#endif
diff --git a/src/test/ex1.txt b/src/test/ex1.txt
new file mode 100644 (file)
index 0000000..00bad94
--- /dev/null
@@ -0,0 +1,12 @@
+This is a test
+
+This is a test1
+This is a test2
+
+
+
+
+
+This is a test3
+This is a test4
+This is a test5
diff --git a/src/test/ex2.txt b/src/test/ex2.txt
new file mode 100644 (file)
index 0000000..d9f7740
--- /dev/null
@@ -0,0 +1,6 @@
+
+
+
+
+b
+
diff --git a/src/test/ex3.txt b/src/test/ex3.txt
new file mode 100644 (file)
index 0000000..5765a5e
--- /dev/null
@@ -0,0 +1,9 @@
+
+
+
+as
+
+  asdfls
+     askdlf
+       a       asdf
+   a    asdf
diff --git a/src/test/test.sh b/src/test/test.sh
new file mode 100755 (executable)
index 0000000..4ff0266
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+TEST_BINARY=./build/typetest
+TEST_DIR=./src/test
+
+find "$TEST_DIR" -name "ex*.txt" -exec "$TEST_BINARY" {} \;
diff --git a/src/typetest.c b/src/typetest.c
new file mode 100644 (file)
index 0000000..708c86f
--- /dev/null
@@ -0,0 +1,213 @@
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <termios.h>
+#include "log.h"
+
+#define C_BLACK   "\x1b[30m"
+#define C_RED     "\x1b[31m"
+#define C_GREEN   "\x1b[32m"
+#define C_YELLOW  "\x1b[33m"
+#define C_BLUE    "\x1b[34m"
+#define C_MAGENTA "\x1b[35m"
+#define C_CYAN    "\x1b[36m"
+#define C_WHITE   "\x1b[37m"
+#define C_NONE    "\x1b[m"
+
+#define LOGLVL LOGLVL_WARN
+
+// Limit maximum number of lines and length of lines
+#define LINE_LENGTH 120
+#define LINE_COUNT 40
+
+
+// Disable terminal echo
+struct termios setnoecho(int term) {
+    struct termios t_orig;
+    tcgetattr(term, &t_orig);
+
+    struct termios t = t_orig;
+    t.c_lflag &= (unsigned int) ~ECHO;
+    tcsetattr(term, TCSANOW, &t);
+
+    return t_orig;
+}
+
+// Restore original terminal settings
+void setecho(int term, struct termios t_orig) {
+    tcsetattr(term, TCSANOW, &t_orig);
+}
+
+// Read lines from file into an array of lines
+char **readlines(char **linearray, unsigned long num_lines, char *filename, bool use_stdin) {
+    FILE *file = NULL;
+
+    ASSERT(linearray != NULL, "No line array provided");
+
+    if (use_stdin) {
+        file = stdin;
+    } else {
+        ASSERT(filename != NULL, "No filename provided");
+        file = fopen(filename, "r");
+        ASSERT(file != NULL, "Failed to open file %s", filename);
+    }
+
+    // Read lines from file into linearray
+    char *line = NULL;
+    for (unsigned int i = 0; !feof(file) && i < num_lines; ++i) {
+        line = calloc(1, LINE_LENGTH * sizeof(char));
+
+        char *r = fgets(line, LINE_LENGTH, file);
+        if (r == NULL) {
+            free(line);
+            continue;
+        }
+        ASSERT(ferror(file) == 0, "Failed to read from file");
+
+        // Don't add blank lines
+        bool no_printable_chars = true;
+        for (unsigned int j = 0; j < LINE_LENGTH; ++j) {
+            if (line[j] == '\0') break;
+            if (isprint(line[j])) {
+                no_printable_chars = false;
+                break;
+            }
+        }
+        if (no_printable_chars || line[0] == '\n') {
+            free(line);
+            --i;
+            continue;
+        }
+        linearray[i] = line;
+        line = NULL;
+    }
+
+    return linearray;
+
+error:
+    if (file != NULL) {
+        ASSERT(fclose(file) != EOF, "Failed to close file");
+    }
+    return NULL;
+}
+
+// Read line of input into a string
+char *getinput(FILE *instream, char *line) {
+    if (fgets(line, LINE_LENGTH, instream) == NULL && feof(instream)) {
+        free(line);
+        return NULL;
+    }
+    return line;
+}
+
+// Compare input to a template string and color characters that are the same
+// green and different characters red
+void checkinput(char *template, char *input) {
+    printf("%s", C_GREEN);
+
+    unsigned long template_len = strlen(template);
+    unsigned long input_len = strlen(input);
+
+    if (input_len == 0) {
+        printf("\n");
+        return;
+    }
+
+    // Ignore leading whitespace
+    bool leading_whitespace = true;
+    for (unsigned long i = 0, j = 0; i < template_len && j < input_len; ++i, ++j) {
+        // Advance to at least first non-whitespace character
+        if (leading_whitespace) {
+            while (!isgraph(template[i]) && i < template_len) {
+                ++i;
+            }
+            while (!isgraph(input[j]) && j < input_len) {
+                ++j;
+            }
+            leading_whitespace = false;
+        }
+
+        if (template[i] == input[j]) {
+            printf("%c", template[i]);
+        } else {
+            printf("%s%c%s", C_RED, input[j], C_GREEN);
+        }
+    }
+    printf("\n%s", C_NONE);
+}
+
+// Interactive typing loop
+void loop(char **linearray, FILE *instream) {
+    char *input = malloc(LINE_LENGTH);
+    for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+        if (linearray[i] == NULL || strcmp(linearray[i], "") == 0) {
+            break;
+        }
+        printf("%s>%s %s%s  ", C_CYAN, C_YELLOW, linearray[i], C_NONE);
+
+        getinput(instream, input);
+        if (input == NULL) return;
+
+        checkinput(linearray[i], input);
+    }
+    free(input);
+}
+
+int main(int argc, char **argv) {
+    bool use_stdin = false;
+
+    if (argc > 2 || (argc == 1 && isatty(0))) {
+        printf("Usage: %s filename\n", argv[0]);
+        return 1;
+    } else {
+        // Reading input from a pipe
+        if (!isatty(0)) {
+            use_stdin = true;
+        }
+    }
+    FILE *termf = NULL;
+
+    char **linearray = calloc(LINE_COUNT, LINE_LENGTH * sizeof(char *));
+    ASSERT(linearray != NULL, "Failed to allocate linearray");
+
+    char **rl = readlines(linearray, LINE_COUNT, argv[1], use_stdin);
+    if (rl == NULL) return 1;
+
+    // Get path for controlling terminal and open fd
+    char termpath[L_ctermid];
+    ctermid(termpath);
+    termf = fopen(termpath, "r+");
+    int term = fileno(termf);
+
+    // Disable echoing inputted characters
+    struct termios t_orig = setnoecho(term);
+
+    // Run the typing test
+    loop(linearray, termf);
+
+    // Reenable echoing
+    setecho(term, t_orig);
+
+    for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+        free(linearray[i]);
+    }
+    free(linearray);
+    if (termf) {
+        if (fclose(termf) == EOF) return 1;
+    }
+    return 0;
+
+error:
+    for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+        free(linearray[i]);
+    }
+    free(linearray);
+    if (termf) {
+        if (fclose(termf) == EOF) return 1;
+    }
+    return 1;
+}