TheodoreAlenas / uni-os-tdd

Tried test driven development for an operating systems University project written in C.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

cd build
make          # or 'make dev', for verbose
./rlr --help
./rlr         # ./rlr -c 2 [...]
ls output
make clean    # cleans the output/ too

1115 2019 00048 Dimakopoulos Theodoros



    - N POSIX semaphores

    - 2 POSIX shared memory segments

    - An improvised priority-stack

    - rlr instances can run in parallel


     ____  ____   ___   ____ _____ ____ ____
    |  _ \|  _ \ / _ \ / ___| ____/ ___/ ___|
    | |_) | |_) | | | | |   |  _| \___ \___ \
    |  __/|  _ <| |_| | |___| |___ ___) |__) |
    |_|   |_| \_\\___/ \____|_____|____/____/


    Parent                Child
    ---------------------------
    init >
                      < saw num

    yes please >
                       < I want
    you're ready >
                    < thank you
    yes please >
                       < I want
    you're ready >
                    < thank you

           ...

                    < thank you

            waitpid


The parent creates the resounces and counts the lines of the file.

     72 | void opening_ceremony(Parent *r) {
     73 |   *( (int *) r->shmem_youre_ready ) = r->lines_in_file;
     74 |
     75 |   WELL("broadcasting the total lines of the file");
     76 |   post_all(r);
     77 |
     78 |   WELL("waiting until they read the number of lines");
     79 |   wait_all(r);
     80 |
     81 |   WELL("signaling that they may start requesting");
     82 |   post_all(r);
     83 | }
    [ file c/parent/interface.c ]

Then, the request loop may begin.
The child leaves its message at a specific part of the segment, and notifies.

     50 |   offset = child->id * MAX_REQUEST_LEN;
     51 |   return shm + offset;
    [ file c/both/shmem.c ]

     36 |   req = write_a_request(child, prev_line);
     37 |   post_and_wait(child, &time_data);
     38 |
     39 |   err = !isolate_line(isolated_line,
     40 |       child->shmem_thank_you, req.line_in_segment);
     41 |   tell_you_got_the_message(child);
     42 |
     43 |   if (err)
     44 |     print_isolate_line_error(child, req);
     45 |   else
     46 |     record_and_wait(child, &time_data, req, isolated_line);
    [ file c/child/loop.c ]

The parent cycles through the "rows" of the children's segments, reading one
message per notification.

     31 |   _sem_wait(s->r->sem_yes_please);
     32 |
     33 |   s->child = copy_and_clear_req(msg_cycler, req_str);
     34 |
     35 |   if (req_says_got_it(req_str))
     36 |     handle_req_saying_got_it(s);
     37 |   else
     38 |     handle_req_saying_i_want(s, req_str);
    [ file c/parent/loop.c ]

The request is written back-to-front.

     96 |   for (i = MAX_REQUEST_LEN - 1; i >= 0; i--)
     97 |     child->shmem_i_want_offset[i] = req_str[i];
    [ file c/child/loop.c ]

Before writing it, it is guaranteed that there was a null character at
position 0. That character should be overriden last, because while the parent
does wait for a semaphore, he doesn't know the source of the semaphore post,
and he will pick up the first child request he finds to be non-empty.

A request might also be an 'I read it' message.

     11 | void req_send_got_it(char *shm) {
     12 |   shm[1] = '\0';
     13 |   shm[0] = '$';
     14 | }
     15 |
     16 | int req_says_got_it(char *msg) {
     17 |   return msg[0] == '$';
     18 | }
    [ file c/both/req.c ]

When a request is about another file segment, it
is stacked, but with priority. It's called
'bubbling' in the code.

     53 |   x1->file_segment = 1;
     54 |   x2->file_segment = 2;
     55 |   x3->file_segment = 1;
     56 |   s = stack_create(3);
     57 |   stack_push(s, x1);
     58 |   stack_push(s, x2);
     59 |   stack_push(s, x3);
     60 |
     61 |   stack_pop(s, &y3);
     62 |   stack_pop(s, &y2);
     63 |   stack_pop(s, &y1);
     64 |   announce("test_stack_1_2_1_is_2_1_1",
     65 |       y1->file_segment == 2 &&
     66 |       y2->file_segment == 1 &&
     67 |       y3->file_segment == 1
     68 |       );
    [ file c/tests/stack.c ]

Then, multiple requests are popped and everyone
interested is notified once the segment changes.

    150 |   stack_for_all_of_segment(s->r->requests,
    151 |       update_readers_and_tell_child, &a);
    [ file c/parent/loop.c ]

     85 |   if (rand() % 8 == 0)
     86 |     line_in_file = rand() % child->lines_in_file;
     87 |   else
     88 |     line_in_file = little_offset(child, *prev_line);
    [ file c/child/loop.c ]


      ___  _   _ ___ ____  _  ______
     / _ \| | | |_ _|  _ \| |/ / ___|
    | | | | | | || || |_) | ' /\___ \
    | |_| | |_| || ||  _ <| . \ ___) |
     \__\_\\___/|___|_| \_\_|\_\____/


     12 | #define WELL(str) PRINT_PREFIX(); printf("%s\n", str)
     13 | #define WELLL(printf_expr) PRINT_PREFIX(); \
     14 |   printf_expr; printf("\n"); fflush(stdout)
    [ file c/both/dev_mode.h ]

     24 | dev: FLAGS = -DDEV
     25 | dev: $(RLR_MAIN_O) main.o
     26 | 	gcc $(RLR_MAIN_O) main.o -o rlr
    [ file build/Makefile ]

'const' isn't used for primitive types because data eventually reaches a
standard function such as readline, which doesn't declare any arguments
constant. So it feels awkward.

Plus I follow a retro style. The long names aren't retro but the declarations
are at the top.

Similarly I avoid unsigned types because they might come from the return value
of a function. Type conversions remove any guarantee unsigned types are
supposed to help with.

There are tiny functions because I'm trying out the "Uncle Bob" principles of
clean coding. The main point is to skip reading code easily. I do rely on
jump-to-definition functionality, though it does break with preprocessor
definitions.


     _____ _____ ____ _____ ____
    |_   _| ____/ ___|_   _/ ___|
      | | |  _| \___ \ | | \___ \
      | | | |___ ___) || |  ___) |
      |_| |_____|____/ |_| |____/


I tried out some test driven development. It didn't go the best but it was
entertaining.

Some .c files are different with  make test

     10 | #ifndef TEST
     11 |
     12 | int testable_fork() {
     13 |   return fork();
     14 | }
    [ file c/before/be_yourself.c ]

      8 | static int num_of_forks;
      9 |
     10 | int testable_fork() {
     11 |   num_of_forks++;
     12 |   return fork();
     13 | }
    [ file c/tests/fork.c ]

The specific example is from a test that was actually written after making the
forks work. There were bugs and any part of the code was suspicious, so I
thought to secure the forks.

The tests aren't very sophisticated, they 'announce' the situation. Also they
never deallocate.

     11 |   announce("errors_on_letter", -1 == req_parse("<2a,4>"));
     12 |   announce("parses_one_digit", 2 == req_parse("<2,4>"));
    [ file c/tests/req.c ]



Code that has to do with the loops of the parent and child is spread out in
multiple files. I'm sorry about it, it evolved gradually this way. Most of it,
however, is in the files called interface and loop. Some is in the be_yourself
files in the before folder. The main reasons are, for one, to separate the
pre-fork parent, the post-fork parent and the children as much as possible, and
secondly to help with testing.


Last update: Mon Dec 19 10:09:44 EET 2022

About

Tried test driven development for an operating systems University project written in C.


Languages

Language:C 94.1%Language:Shell 4.2%Language:Makefile 1.7%