Skip to main content
Compilation Toolchain
Compilation Toolchain
IOT
3h

Session 4 — Git Advanced + Code Quality: Instructor Print Guide

Instructor material — not student-facing

Single-page print reference: all demo files and terminal commands in order.


Setup

cd ~/lecture/session4/

Demo Files

ugly.c

#include <stdio.h>
#include<stdlib.h>
#include <string.h>

#define TAILLE 100

// compute the average
double    moyenne(int* tab,int n){
double s=0;int i;
    for(i=0;i<n;i++){
  s=s+tab[i];
        }
return s/n;
}

//find max
int maximum(int* t  ,  int    n)
{
int max=t[0];
for(int i=1;i<n;i++){
if(t[i]>max)max=t[i];
}
return max;
}

// identical to maximum but for minimum — violates DRY
int minimum(int* t,int n)
{
int min=t[0];
for(int i=1;i<n;i++){
if(t[i]<min)min=t[i];
}
return min;
}

char* concat(char* a,char* b){
char* result=malloc(strlen(a)+strlen(b)+1);
strcpy(result,a);
strcat(result,b);
return result;
}

void print_tab(int tab[],int n)
{
    int i;
printf("[");
for(i=0;i<n;i++){
if(i>0)printf(", ");
printf("%d",tab[i]);}
printf("]\n");
}

int main()
{
int tab[TAILLE];
int i;
for(i=0;i<10;i++)tab[i]=rand()%100;

printf("Array: ");
print_tab(tab,10);

printf("Average: %.2f\n",moyenne(tab,10));
printf("Max: %d\n",maximum(tab,10));
printf("Min: %d\n",minimum(tab,10));

char* msg=concat("Hello ","World!");
printf("%s\n",msg);
// memory leak: msg is never freed

int x;
printf("Uninitialized: %d\n",x);

return 0;
}

Issues to spot: inconsistent spacing/braces, #include<stdlib.h> missing space, main() not main(void), memory leak (msg never freed), x used uninitialized, min/max near-duplicates (DRY violation), missing const on read-only pointer params.


buggy.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Bug 1: heap-use-after-free */
void use_after_free(void)
{
    char *buffer = malloc(16);
    strcpy(buffer, "hello");
    free(buffer);
    printf("After free: %s\n", buffer);  /* BUG: use after free */
}

/* Bug 2: heap-buffer-overflow */
void buffer_overflow(void)
{
    int *arr = malloc(5 * sizeof(int));
    for (int i = 0; i <= 5; i++)         /* BUG: off-by-one, writes arr[5] */
        arr[i] = i * 10;
    printf("arr[4] = %d\n", arr[4]);
    free(arr);
}

/* Bug 3: memory leak */
void leak(void)
{
    char *data = malloc(1024);
    strcpy(data, "this is leaked");
    printf("Leaked data: %s\n", data);
    /* BUG: no free(data) */
}

/* Bug 4: double free */
void double_free(void)
{
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    /* free(p);  — uncomment to demo double free (crashes immediately) */
}

/* Bug 5: stack-buffer-overflow */
void stack_overflow(void)
{
    int arr[10];
    for (int i = 0; i <= 10; i++)        /* BUG: writes arr[10] */
        arr[i] = i;
    printf("arr[9] = %d\n", arr[9]);
}

int main(void)
{
    printf("=== Bug 1: use-after-free ===\n");
    use_after_free();

    /* Uncomment one at a time — ASan stops at the first error */
    /*
    printf("=== Bug 2: heap-buffer-overflow ===\n");
    buffer_overflow();

    printf("=== Bug 3: memory leak ===\n");
    leak();

    printf("=== Bug 4: double free ===\n");
    double_free();

    printf("=== Bug 5: stack-buffer-overflow ===\n");
    stack_overflow();
    */

    return 0;
}

.clang-format

---
Language: C
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
BreakBeforeBraces: Allman
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
PointerAlignment: Right
SpaceBeforeParens: ControlStatements
...

.clang-tidy

---
Checks: >
    -*,
    bugprone-*,
    cert-*,
    clang-analyzer-*,
    misc-*,
    performance-*,
    readability-braces-around-statements,
    readability-identifier-naming,
    readability-implicit-bool-conversion,
    readability-misleading-indentation
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
CheckOptions:
    - key: readability-identifier-naming.FunctionCase
      value: lower_case
    - key: readability-identifier-naming.VariableCase
      value: lower_case
    - key: readability-identifier-naming.MacroDefinitionCase
      value: UPPER_CASE
...

4.1 — Git Quick Recap (5 min)

git init demo && cd demo
echo "int main() { return 0; }" > main.c
git add main.c && git commit -m "Initial commit"
git log --oneline

4.2 — Branches (20 min)

# Create and switch to a feature branch
git checkout -b feature/add-utils

# Make changes
cat > utils.c << 'EOF'
int add(int a, int b) { return a + b; }
EOF
git add utils.c && git commit -m "Add utils module"

# Show the branch graph
git log --oneline --graph --all

# Switch back to main
git checkout master
ls       # utils.c is gone!

# Make a change on master too
echo "// updated" >> main.c
git add main.c && git commit -m "Update main"

# Show divergence
git log --oneline --graph --all

Draw the DAG on the board: branches are just pointers to commits.


4.3 — Merging and Conflict Resolution (25 min)

# Setup: both branches modify the same line
git checkout master
echo 'int main() { return 0; }' > main.c
git add main.c && git commit -m "Reset main"

git checkout -b feature/return-one
sed -i 's/return 0/return 1/' main.c
git add main.c && git commit -m "Return 1"

git checkout master
sed -i 's/return 0/return 42/' main.c
git add main.c && git commit -m "Return 42"

# Now merge — conflict!
git merge feature/return-one
# CONFLICT!
Warning

▶ Wooclap Q8

Show the conflicted file on the projector. Fire before editing.

<<<<<<< HEAD
int main() { return 42; }
=======
int main() { return 1; }
>>>>>>> feature/return-one
# 1. See the conflict markers
cat main.c

# 2. Edit to resolve
vim main.c

# 3. Mark as resolved
git add main.c
git commit    # default merge commit message

# 4. Verify
git log --oneline --graph --all

4.4 — Pull / Merge Requests (15 min)

# Local side
git checkout -b feature/new-feature
# make changes, commit
git push -u origin feature/new-feature

Switch to GitLab UI (screen share):

  1. Create Merge Request — title, description, assignee, reviewer
  2. Show the diff view and inline comments
  3. CI pipeline runs on the MR
  4. Reviewer approves → merge → branch deleted

“MRs are about code review and quality gates, not just merging.”


4.5 — NASA “Power of 10” (5 min)

#RuleWhy it matters
1No goto, setjmp, or longjmpKeeps control flow predictable and analyzable
2All loops must have a fixed upper boundPrevents infinite hangs
6Declare variables at the smallest possible scopeReduces stale-state bugs
7Check the return value of all non-void functionsA failed malloc or fopen can crash a satellite
Warning

▶ Wooclap Q9

Show this snippet on the projector. Fire before explaining.

int x;
printf("Uninitialized: %d\n", x);

4.6 — Code Formatting with clang-format (10 min)

cd session4/

# Show ugly code
cat ugly.c

# Format it
clang-format ugly.c          # prints to stdout
clang-format -i ugly.c       # in-place

# Show the config file
cat .clang-format

# Generate one from a preset
clang-format -style=llvm -dump-config > .clang-format

Add to Makefile:

format:
	clang-format -i src/*.c include/*.h

4.7 — Static Analysis with clang-tidy (10 min)

Warning

▶ Wooclap Q10

Show ugly.c on the projector (full file). Fire before running clang-tidy.

Students guess the number of warnings.

# Run on the file with issues
clang-tidy --quiet -checks='*' ugly.c --

# Check categories:
# - bugprone-*: likely bugs
# - cert-*: CERT secure coding
# - readability-*: code clarity
# - modernize-*: modern C practices

# Use a config file
cat .clang-tidy

# Fix automatically
clang-tidy --fix ugly.c --

Comparison: gcc = 0 warnings, gcc -Wall = 6, clang-tidy = 88.


4.8 — Memory Checking (5 min)

# Compile with AddressSanitizer
clang -W -Wall -g -fsanitize=address buggy.c -o buggy
./buggy
# → AddressSanitizer: heap-use-after-free (exact line + stack trace)

# For leak detection specifically
clang -W -Wall -g -fsanitize=address -fsanitize=leak buggy.c -o buggy
./buggy

Uncomment bugs in buggy.c one at a time — ASan stops at the first error.


4.9 — Code Smells Catalogue (40 min)

Setup

cd ~/lecture/session4/smells/

Five focused files, each isolating one smell family. Open each in the editor and walk through them on the projector. After each file run the toolchain so students can see the warning count jump.


File 1 — smell_uninit.c (10 min)

smell_uninit.c
/* smell_uninit.c — Code smell: uninitialized variables
 *
 * All bugs below will be SILENTLY COMPILED with plain gcc.
 * Run: gcc smell_uninit.c -W -Wall -Wextra   to see warnings.
 * Run: clang-tidy --quiet -checks='*' smell_uninit.c --  for more.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---------------------------------------------------------------
 * Smell 1: plain local variable used before initialization.
 * GCC produces a warning only with -Wuninitialized (part of -Wall).
 * The value is whatever was on the stack — garbage.
 * --------------------------------------------------------------- */
void smell1_plain_local(void)
{
    int result;          /* declared but never assigned */
    printf("result = %d\n", result);   /* undefined behaviour */
}

/* ---------------------------------------------------------------
 * Smell 2: only some paths initialize the variable.
 * gcc -Wmaybe-uninitialized catches this; plain gcc misses it.
 * --------------------------------------------------------------- */
int smell2_partial_init(int x)
{
    int v;               /* not initialized unconditionally */
    if (x > 0)
        v = x * 2;
    /* if x <= 0, v is still garbage */
    return v;            /* may return garbage */
}

/* ---------------------------------------------------------------
 * Smell 3: switch without default — variable may stay unset.
 * --------------------------------------------------------------- */
const char *smell3_switch_no_default(int code)
{
    const char *msg;     /* uninitialized */
    switch (code)
    {
        case 0: msg = "zero";     break;
        case 1: msg = "one";      break;
        case 2: msg = "two";      break;
        /* no default: msg is uninitialized for any other code */
    }
    return msg;          /* UB if code not in {0,1,2} */
}

/* ---------------------------------------------------------------
 * Smell 4: pointer used before malloc / before NULL check.
 * Dereferencing without checking malloc return = crash or worse.
 * --------------------------------------------------------------- */
void smell4_ptr_no_null_check(int n)
{
    int *buf;
    buf = malloc(n * sizeof(int));
    /* BUG: no NULL check — if malloc fails, next line crashes */
    buf[0] = 42;
    free(buf);
}

/* ---------------------------------------------------------------
 * Smell 5: pointer declared, never assigned, then freed.
 * This is undefined behaviour — the free() call is on garbage.
 * --------------------------------------------------------------- */
void smell5_free_uninit_ptr(void)
{
    char *p;             /* declared but never assigned */
    /* ... some code that "forgets" to assign p ... */
    free(p);             /* UB: freeing a garbage pointer */
}

/* ---------------------------------------------------------------
 * Smell 6: struct fields silently left at 0/garbage.
 * Using = {0} for zero-init is not always enough (e.g. pointers
 * on exotic platforms), but missing = {0} is clearly wrong.
 * --------------------------------------------------------------- */
typedef struct
{
    int    width;
    int    height;
    char  *label;
    double ratio;
} Box;

void smell6_partial_struct_init(void)
{
    Box b;               /* all fields uninitialized */
    b.width = 10;        /* only width set */
    /* height, label, ratio are garbage */
    printf("box: %d x %d  label=%s  ratio=%.2f\n",
           b.width, b.height, b.label, b.ratio);
}

/* ---------------------------------------------------------------
 * Smell 7: loop variable reused without reset between loops.
 * The second loop starts from wherever i ended — logic bug.
 * --------------------------------------------------------------- */
void smell7_loop_var_reuse(void)
{
    int i;
    int a[5] = {1, 2, 3, 4, 5};
    int b[3] = {10, 20, 30};

    for (i = 0; i < 5; i++)
        printf("a[%d]=%d ", i, a[i]);
    printf("\n");

    /* BUG: i still equals 5 from the previous loop.
     * The second loop never executes! */
    for (; i < 3; i++)
        printf("b[%d]=%d ", i, b[i]);
    printf("\n");
}

/* ---------------------------------------------------------------
 * Smell 8: VLA declared, but size comes from uninitialized var.
 * This is particularly dangerous — any array size is possible.
 * --------------------------------------------------------------- */
void smell8_vla_garbage_size(void)
{
    int n;               /* uninitialized — could be negative! */
    /* The line below may crash, allocate billions of bytes,
     * or silently corrupt the stack depending on what n holds. */
    int arr[n];          /* VLA with garbage size */
    arr[0] = 1;
}

int main(void)
{
    smell1_plain_local();
    printf("partial_init(5)  = %d\n", smell2_partial_init(5));
    printf("partial_init(-1) = %d\n", smell2_partial_init(-1)); /* UB */
    printf("switch msg       = %s\n", smell3_switch_no_default(0));
    printf("switch bad code  = %s\n", smell3_switch_no_default(99)); /* UB */
    smell4_ptr_no_null_check(10);
    smell6_partial_struct_init();
    smell7_loop_var_reuse();
    /* smell5 and smell8 would crash — left commented out */
    return 0;
}

Smells highlighted:

#LocationIssue
1smell1_plain_localresult used before any assignment
2smell2_partial_initv set only when x > 0 — garbage on the false branch
3smell3_switch_no_defaultmsg stays unset for unexpected code values
4smell4_ptr_no_null_checkmalloc return unchecked — crash on OOM
5smell5_free_uninit_ptrfree() called on garbage pointer
6smell6_partial_struct_initStruct fields left uninitialized — printed as garbage
7smell7_loop_var_reusei reused across two loops — second loop never runs
8smell8_vla_garbage_sizeVLA size from uninitialized n — stack corruption
# Baseline: gcc with no flags
gcc smell_uninit.c
./a.out           # runs but produces garbage values

# Enable warnings
gcc smell_uninit.c -W -Wall -Wextra
# Spots smells 1, 2, 3, 6, 7

# Static analysis
clang-tidy --quiet -checks='*' smell_uninit.c --
# Spots smells 1-8 + additional CERT violations

# Compile with UBSan to catch at runtime
clang -W -Wall -g -fsanitize=undefined smell_uninit.c -o smell_uninit
./smell_uninit
Info

Teaching tip: Ask students to predict which smells gcc -Wall catches vs. which only clang-tidy catches. The gap is usually surprising.


File 2 — smell_layout.c (8 min)

smell_layout.c
/* smell_layout.c — Code smell: bad formatting and layout
 *
 * Issues demonstrated:
 *  - Extremely long lines (200+ characters)
 *  - Mixed indentation (tabs vs spaces, 2 vs 4 vs 8 spaces)
 *  - Mixed brace placement styles in the same file
 *  - Multiple statements crammed on one line
 *  - Operators with no spaces, or inconsistent spacing
 *  - Magic numbers scattered everywhere
 *  - Return type on a separate line from function name (old K&R style)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

/* ---------------------------------------------------------------
 * Smell 1: extremely long lines — no one can read this without
 * horizontal scrolling.  The 80/100/120 column limit exists for
 * a reason.
 * --------------------------------------------------------------- */
void process_image(unsigned char input_matrix[1024][1024], unsigned char output_matrix[1024][1024], int width, int height, int threshold, int mode) { for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { if (mode == 0) { output_matrix[row][col] = (input_matrix[row][col] > threshold) ? 255 : 0; } else if (mode == 1) { output_matrix[row][col] = (unsigned char)(0.299 * input_matrix[row][col] + 0.587 * input_matrix[row][col] + 0.114 * input_matrix[row][col]); } else { output_matrix[row][col] = input_matrix[row][col]; } } } }

/* ---------------------------------------------------------------
 * Smell 2: mixed brace style — Allman for one function, K&R for
 * the next, Egyptian for a third.  Pick one and stick with it.
 * --------------------------------------------------------------- */

/* Allman style */
int compute_sum(int *array, int size)
{
    int total = 0;
    for (int i = 0; i < size; i++)
    {
        total += array[i];
    }
    return total;
}

/* K&R style — inconsistent with the function above */
int compute_max(int *array, int size) {
    int m = array[0];
    for (int i = 1; i < size; i++) {
        if (array[i] > m) { m = array[i]; }
    }
    return m;
}

/* No braces at all on multi-line if/else — easy to misread */
int compute_min(int *array, int size)
{
    int m = array[0];
    for (int i = 1; i < size; i++)
        if (array[i] < m)
            m = array[i];
    return m;
}

/* ---------------------------------------------------------------
 * Smell 3: inconsistent indentation — 2 spaces, then 4, then tabs.
 * Many editors will show this as completely different widths.
 * --------------------------------------------------------------- */
void print_matrix(int rows, int cols, double matrix[rows][cols])
{
  for (int i = 0; i < rows; i++) {    /* 2-space indent */
      for (int j = 0; j < cols; j++) {  /* 4-space indent */
	      printf("%6.2f ", matrix[i][j]);  /* tab indent */
      }
    printf("\n");   /* back to 2-space */
  }
}

/* ---------------------------------------------------------------
 * Smell 4: operators with no spaces — looks like a formula from
 * a 1970s Fortran book.
 * --------------------------------------------------------------- */
double quadratic(double a,double b,double c,double x){return a*x*x+b*x+c;}

double discriminant(double a,double b,double c){
double d=b*b-4*a*c;
return d;
}

/* ---------------------------------------------------------------
 * Smell 5: multiple statements on one line everywhere.
 * This makes it impossible to set breakpoints on individual steps
 * and hides the logic flow completely.
 * --------------------------------------------------------------- */
void compress_array(int *src, int *dst, int n, int factor)
{
    int i=0,j=0,sum=0,count=0;
    while(i<n){sum+=src[i];count++;i++;if(count==factor){dst[j]=sum/factor;j++;sum=0;count=0;}}
    if(count>0){dst[j]=sum/count;}
}

/* ---------------------------------------------------------------
 * Smell 6: old K&R-style function declaration (no parameter types
 * in the signature line — types come separately).  This predates
 * ANSI C and gives the compiler zero information to check calls.
 * --------------------------------------------------------------- */
int old_style_add(a, b)
int a;
int b;
{
    return a + b;
}

/* ---------------------------------------------------------------
 * Smell 7: misaligned closing braces and random blank lines that
 * break the visual grouping of code blocks.
 * --------------------------------------------------------------- */
void state_machine(int input)
{
    static int state = 0;

    if (state == 0)
    {
        if (input == 1) { state = 1;
        printf("0→1\n"); }
        else { printf("stay 0\n");
            }
    } else if (state == 1) {
            if (input == 0) {
        state = 0; printf("1→0\n"); }

    else { state = 2; printf("1→2\n");
    }
    }
    else
    {
    printf("state 2 — reset\n");
    state=0;
            }
}

/* ---------------------------------------------------------------
 * Smell 8: magic numbers with no explanation.
 * What is 0.2126? 0.7152? 0.0722? (they are luma coefficients)
 * What is 255? What is 16, 235? (TV range vs full range)
 * --------------------------------------------------------------- */
unsigned char to_luma(unsigned char r, unsigned char g, unsigned char b)
{
    double luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
    if (luma < 16)  luma = 16;
    if (luma > 235) luma = 235;
    return (unsigned char)luma;
}

int main(void)
{
    int arr[] = {5, 3, 8, 1, 9, 2};
    int n = 6;
    printf("sum=%d max=%d min=%d\n",
           compute_sum(arr, n), compute_max(arr, n), compute_min(arr, n));
    printf("quad(1,0,-1,3)=%.1f\n", quadratic(1, 0, -1, 3));
    printf("luma(200,100,50)=%d\n", to_luma(200, 100, 50));
    return 0;
}

Smells highlighted:

#LocationIssue
1process_imageEntire function body on one 300-character line
2compute_sum / compute_max / compute_minThree brace styles in three consecutive functions
3print_matrix2-space, 4-space, and tab indentation mixed inside one function
4quadratic, discriminantNo spaces around operators — unreadable formula
5compress_arraySix statements on one line inside a while
6old_style_addK&R parameter declaration style (deprecated since C99)
7state_machineMisaligned closing braces; random blank lines
8to_lumaMagic numbers 0.2126, 0.7152, 0.0722, 16, 235
# Format check — exits 1 if anything differs
clang-format --dry-run --Werror smell_layout.c
# → spits out a huge diff

# Fix it in-place
clang-format -i smell_layout.c

# The function on one line becomes ~20 readable lines
# Magic numbers would need manual refactoring (not automated)
Info

Teaching tip: clang-format fixes whitespace/layout automatically but cannot name your constants. Show students the before/after diff with git diff.


File 3 — smell_naming.c (8 min)

smell_naming.c
/* smell_naming.c — Code smell: bad, inconsistent, and cryptic naming
 *
 * Issues demonstrated:
 *  - Single-letter names outside of trivial loop indices
 *  - Inconsistent casing: camelCase vs snake_case vs ALLCAPS for non-macros
 *  - Generic / meaningless names: tmp, temp, data, val, res, ret, buf, ptr
 *  - Over-abbreviated names that require context to decode
 *  - Function names that do not describe what the function does
 *  - Boolean variables named as verbs with confusing polarity (isNotDone)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

/* ---------------------------------------------------------------
 * Smell 1: single-letter names for non-trivial roles.
 * What does 'n' hold? What is 'r' the result of? 'f'? 'a', 'b'?
 * --------------------------------------------------------------- */
double f(double a, double b, double n)
{
    double r = 0;
    double x = a;
    double h = (b - a) / n;   /* h is OK here — well-known in math */
    while (x < b)
    {
        r += (x * x + 2 * x + 1) * h;  /* what polynomial is this? */
        x += h;
    }
    return r;
}

/* ---------------------------------------------------------------
 * Smell 2: inconsistent naming conventions in the same file.
 * camelCase function next to snake_case function next to ALLCAPS.
 * --------------------------------------------------------------- */
int computeAverage(int *array, int size)   /* camelCase */
{
    int total = 0;
    for (int i = 0; i < size; i++) total += array[i];
    return total / size;
}

int find_median(int *array, int size)      /* snake_case */
{
    /* (assumes sorted) */
    return array[size / 2];
}

int FINDMAX(int *T, int N)                 /* ALLCAPS — not a macro! */
{
    int M = T[0];
    for (int I = 1; I < N; I++)
        if (T[I] > M) M = T[I];
    return M;
}

/* ---------------------------------------------------------------
 * Smell 3: generic meaningless names — every variable is tmp/data/res.
 * Reading this function requires re-deriving what each variable means.
 * --------------------------------------------------------------- */
int *process(int *data, int tmp, int res)
{
    int *ret = malloc(tmp * sizeof(int));
    if (ret == NULL) return NULL;

    int val = 0;
    for (int i = 0; i < tmp; i++)
    {
        val = data[i] * res;
        ret[i] = val;
    }
    return ret;
}

/* ---------------------------------------------------------------
 * Smell 4: over-abbreviated names.
 * nb_elt? lng? buf_sz? blk_cnt? Abbreviated to the point of cryptic.
 * --------------------------------------------------------------- */
int cpy_blk(char *dst, const char *src, int nb_elt, int blk_sz, int buf_sz)
{
    int lng    = 0;
    int blk_cnt = 0;

    while (lng < nb_elt && blk_cnt * blk_sz < buf_sz)
    {
        memcpy(dst + blk_cnt * blk_sz, src + blk_cnt * blk_sz, blk_sz);
        lng    += blk_sz;
        blk_cnt++;
    }
    return blk_cnt;
}

/* ---------------------------------------------------------------
 * Smell 5: boolean variable with negative/confusing polarity.
 * Double negatives in conditions are a reading trap.
 * --------------------------------------------------------------- */
void pump_events(int max_iter)
{
    int isNotDone  = 1;    /* confusing: we need NOT NOT done to continue */
    int noError    = 1;    /* same issue */
    int iter       = 0;

    while (isNotDone && noError)
    {
        if (!isNotDone) break;  /* dead code because of the while condition */
        iter++;
        if (iter >= max_iter) isNotDone = 0;  /* double-negative assignment */
    }
}

/* ---------------------------------------------------------------
 * Smell 6: function names that describe HOW, not WHAT.
 * "doLoop", "doStuff", "helper", "util", "run" give zero information.
 * --------------------------------------------------------------- */
void doLoop(int *arr, int n)   /* what loop? for what purpose? */
{
    for (int i = 0; i < n; i++)
        arr[i] *= 2;
}

int helper(int a, int b)       /* helper for what? */
{
    return (a > b) ? a : b;
}

void doStuff(void)             /* the worst possible name */
{
    printf("doing stuff\n");
}

/* ---------------------------------------------------------------
 * Smell 7: shadowing — local variable has the same name as
 * a global/outer variable.  Which 'count' is being modified?
 * --------------------------------------------------------------- */
int count = 0;   /* global */

void increment(void)
{
    int count = 0;     /* shadows the global — bug hiding in plain sight */
    count++;           /* only increments the local copy */
    printf("local count = %d, global count = %d\n", count, count);
    /* the two 'count' in printf both refer to the local! */
}

/* ---------------------------------------------------------------
 * Smell 8: type-Hungarian notation applied inconsistently.
 * Some vars have a prefix (pBuf, iLen), others don't — so the
 * prefix adds noise without consistency benefits.
 * --------------------------------------------------------------- */
void Hungarian(char *pBuf, int iLen, int offset)
{
    int i;      /* no prefix */
    char c;     /* no prefix */
    int nBytes = iLen - offset;   /* partial prefix */

    for (i = 0; i < nBytes; i++)
    {
        c = pBuf[i + offset];
        printf("%c", c);
    }
    printf("\n");
}

int main(void)
{
    int arr[] = {4, 1, 7, 2, 9, 3, 8};
    int n     = 7;

    printf("f(0,1,1000) = %.4f\n", f(0, 1, 1000));
    printf("avg   = %d\n", computeAverage(arr, n));
    printf("max   = %d\n", FINDMAX(arr, n));

    int src_data[] = {1, 2, 3, 4, 5};
    int *out = process(src_data, 5, 3);
    if (out) { free(out); }

    increment();
    return 0;
}

Smells highlighted:

#LocationIssue
1f(a, b, n)Single-letter parameters for a numerical integration routine
2computeAverage / find_median / FINDMAXThree different casing conventions in the same file
3process(data, tmp, res)Three generic names — reader must reverse-engineer meaning
4cpy_blk(nb_elt, blk_sz, buf_sz, lng)Over-abbreviated to the point of cryptic
5isNotDone, noErrorNegative boolean names create double negatives in conditions
6doLoop, helper, doStuffFunction names that describe nothing
7count (global vs local)Local variable shadows the global — modification is silently lost
8HungarianType prefix applied inconsistently (mix of prefixed and unprefixed)
# clang-tidy with identifier naming rules
clang-tidy --quiet \
    -checks='readability-identifier-naming' \
    -config='{CheckOptions: [
        {key: readability-identifier-naming.FunctionCase, value: lower_case},
        {key: readability-identifier-naming.VariableCase,  value: lower_case}
    ]}' \
    smell_naming.c --

# Detect shadowing
gcc smell_naming.c -W -Wall -Wshadow
# → warning: declaration of 'count' shadows a global declaration
Warning

▶ Wooclap Q11

Show process(data, tmp, res) on the projector. What does this function do?

Students vote: (a) scales an array, (b) copies an array, (c) sums an array, (d) impossible to tell without reading the body.


File 4 — smell_includes.c (7 min)

smell_includes.c
/* smell_includes.c — Code smell: #include directives not at the top
 *
 * The C standard requires declarations to be in scope before use,
 * but nothing technically forbids putting #include in the middle of
 * a file.  However, this is a severe readability and maintenance issue:
 *  - Readers don't know which headers are available where.
 *  - Conditional or late includes can cause subtle ordering bugs.
 *  - Static checkers, IDEs, and code formatters assume includes come first.
 *
 * All "smells" below are deliberately placed in the wrong location.
 */

#include <stdio.h>   /* OK — this is where includes should be */

/* A global constant — fine so far */
#define BUFFER_SIZE 256
#define PI 3.14159265358979

/* ---------------------------------------------------------------
 * Smell 1: #include buried after some declarations.
 * The programmer added stdlib.h "when they needed malloc" rather
 * than gathering all includes at the top.
 * --------------------------------------------------------------- */
static int g_counter = 0;

#include <stdlib.h>  /* BAD: should be at the top with stdio.h */

void increment_counter(void)
{
    g_counter++;
}

int get_counter(void)
{
    return g_counter;
}

/* ---------------------------------------------------------------
 * Smell 2: #include after function definitions.
 * string.h is only needed further down but is included here,
 * between two function groups — confusing for anyone reading top-down.
 * --------------------------------------------------------------- */
double circle_area(double radius)
{
    return PI * radius * radius;
}

double sphere_volume(double radius)
{
    return (4.0 / 3.0) * PI * radius * radius * radius;
}

#include <string.h>  /* BAD: mid-file, after geometry functions */

char *safe_duplicate(const char *src)
{
    if (src == NULL) return NULL;
    char *copy = malloc(strlen(src) + 1);
    if (copy == NULL) return NULL;
    strcpy(copy, src);
    return copy;
}

/* ---------------------------------------------------------------
 * Smell 3: #define used before the matching #include that would
 * normally provide the type.  Here the programmer added the
 * #include only when the compiler complained, inserting it at the
 * point of use rather than the top of the file.
 * --------------------------------------------------------------- */
#define MAX_FILES 16

typedef struct
{
    int   fd;
    char  name[BUFFER_SIZE];
    int   is_open;
} FileEntry;

#include <errno.h>   /* BAD: should be at the top */

int open_file_entry(FileEntry *entry, const char *name)
{
    if (entry == NULL || name == NULL)
    {
        errno = EINVAL;
        return -1;
    }
    strncpy(entry->name, name, BUFFER_SIZE - 1);
    entry->name[BUFFER_SIZE - 1] = '\0';
    entry->is_open = 0;
    entry->fd      = -1;
    return 0;
}

/* ---------------------------------------------------------------
 * Smell 4: conditional #include inside a function body.
 * This compiles on some platforms/compilers but is never correct
 * style and is explicitly undefined behaviour in C11+.
 * --------------------------------------------------------------- */
void print_platform_info(void)
{
#include <stdint.h>   /* VERY BAD: #include inside a function */
    uint32_t magic = 0xDEADBEEF;
    printf("magic = 0x%08X\n", magic);
    printf("sizeof(uint32_t) = %zu\n", sizeof(uint32_t));
}

/* ---------------------------------------------------------------
 * Smell 5: recursive / circular include chain simulated inline.
 * Including a header that pulls in another header that could pull
 * back in the current file — avoided here by guards, but the
 * smell is placing the #include so late that guards are not
 * checked until execution is logically "inside" the file.
 * --------------------------------------------------------------- */
int compute_hash(const char *s)
{
    unsigned int h = 5381;
    int c;
    while ((c = *s++) != '\0')
        h = ((h << 5) + h) + (unsigned int)c;  /* djb2 */
    return (int)(h % 65521);
}

#include <math.h>    /* BAD: used only in the last function, added late */

double normalize(double value, double min_val, double max_val)
{
    if (fabs(max_val - min_val) < 1e-9)
        return 0.0;
    return (value - min_val) / (max_val - min_val);
}

int main(void)
{
    increment_counter();
    increment_counter();
    printf("counter = %d\n", get_counter());

    printf("circle area r=3: %.4f\n", circle_area(3.0));

    char *dup = safe_duplicate("hello world");
    printf("duplicate: %s\n", dup);
    free(dup);

    FileEntry fe;
    open_file_entry(&fe, "test.txt");
    printf("file entry name: %s\n", fe.name);

    print_platform_info();

    printf("hash(\"hello\") = %d\n", compute_hash("hello"));
    printf("normalize(5, 0, 10) = %.2f\n", normalize(5.0, 0.0, 10.0));

    return 0;
}

Smells highlighted:

#LocationIssue
1After g_counterstdlib.h included after the first global — late discovery
2Between geometry functionsstring.h included in the middle of the file
3After MAX_FILES defineerrno.h included after types and macros that use it
4Inside print_platform_info()stdint.h included inside a function body
5End of filemath.h added where fabs() first appears
# See the include order clang would prefer
clang -W -Wall -Weverything smell_includes.c -o /dev/null 2>&1 | grep include

# clang-tidy catches the function-body include
clang-tidy --quiet -checks='llvm-include-order,*' smell_includes.c --

# Correct version: move ALL includes to the very top, sorted:
#   <stdlib.h> <stdio.h> <string.h> <errno.h> <math.h> <stdint.h>
Info

Rule of thumb: Standard library headers first (alphabetically), then your own project headers, all at the very top of the file, before any code.


File 5 — smell_combined.c (7 min)

smell_combined.c
/* smell_combined.c — Realistic "student project" accumulating ALL smells
 *
 * This file simulates a real student submission that was written quickly,
 * never reviewed, and never run through any quality tool.
 *
 * Smells present (for instructor annotation on projector):
 *  [U] Uninitialized variable
 *  [L] Layout / formatting issue
 *  [N] Naming issue
 *  [I] Include not at top
 *  [D] DRY violation (duplicated logic)
 *  [M] Memory issue (leak / overflow / UB)
 *
 * Compile with:
 *    gcc smell_combined.c -W -Wall -Wextra -o smell_combined
 * Then:
 *    clang-tidy --quiet -checks='*' smell_combined.c --
 */

#include <stdio.h>

#define TMAX 50
#define NMAX 20

/* [N] Inconsistent naming: camelCase next to snake_case */
/* [L] No blank lines to separate logical sections         */
typedef struct{int x;int y;char nom[32];int valeur;}Point;   /* [L] cramped */
typedef struct{Point pts[NMAX];int nb;}PointSet;

#include <string.h>  /* [I] mid-file include */

/* [N] Single-letter parameter names with no obvious meaning */
/* [L] One-liner that should be expanded for readability     */
double dist(Point a,Point b){return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);}

/* [D] Duplicated logic: almost identical to dist() above */
double Distance(Point p1,Point p2){    /* [N] inconsistent casing */
double dx=p1.x-p2.x;double dy=p1.y-p2.y;   /* [L] two stmts on one line */
return dx*dx+dy*dy;}

/* [N][L] Meaningless name, everything on one line */
int f(PointSet*s,int v){int r=0;for(int i=0;i<s->nb;i++)if(s->pts[i].valeur==v)r++;return r;}

#include <stdlib.h>  /* [I] mid-file include */

/* [N] "process" tells us nothing; [U] res uninitialized on some paths */
int process(PointSet *s, int mode)
{
    int res;                    /* [U] uninitialized */
    if(mode==0){                /* [L] no space before { */
        int sum=0;
        for(int i=0;i<s->nb;i++)sum+=s->pts[i].valeur;
        res=sum;
    }
    else if(mode==1){
        int mx=s->pts[0].valeur;   /* [U] UB if s->nb == 0 */
        for(int i=1;i<s->nb;i++)if(s->pts[i].valeur>mx)mx=s->pts[i].valeur;
        res=mx;
    }
    /* [U] if mode is anything else, res is still garbage */
    return res;
}

/* [N] "do_truc" is meaningless French slang */
/* [L] Mix of 2-space and 4-space indentation */
void do_truc(PointSet *s, char *Nom, int V)  /* [N] Nom is camelCase, V is ALLCAPS */
{
  Point p;           /* [L] 2-space indent inside function */
  p.x=rand()%100;p.y=rand()%100;  /* [L] two stmts on one line; [U] no srand */
    strcpy(p.nom,Nom);  /* [L] 4-space indent now — inconsistent; [M] no bounds check */
  p.valeur=V;
  if(s->nb<NMAX)      /* [L] no space after if */
  {
    s->pts[s->nb]=p;
      s->nb++;        /* [L] 6-space indent — different again */
  }
}

/* [D] Duplicated printing logic — nearly identical to print_set below */
void AfficherPoints(PointSet *s)   /* [N] French + camelCase */
{
    for(int i=0;i<s->nb;i++)
        printf("(%d,%d) %s val=%d\n",s->pts[i].x,s->pts[i].y,s->pts[i].nom,s->pts[i].valeur);
}

/* [D] Almost the same function with a slightly different format string */
void print_set(PointSet *s)
{
    for(int i=0;i<s->nb;i++)
        printf("[%d,%d] '%s' v=%d\n",s->pts[i].x,s->pts[i].y,s->pts[i].nom,s->pts[i].valeur);
}

#include <math.h>   /* [I] late include — only used in find_closest */

/* [N] Return type on its own line (old K&R style) */
/* [M] Returns pointer to first match — but what if no match? returns NULL  */
/* (actually: uninitialized 'closest' if s->nb==0) */
Point *
find_closest(PointSet *s, Point ref)  /* [L] function name split across lines */
{
    Point *closest;    /* [U] uninitialized pointer — UB if nb==0 */
    double dMin=1e18;
    for(int i=0;i<s->nb;i++){
        double d=sqrt(dist(s->pts[i],ref));  /* [D] dist() already squared — sqrt is wrong */
        if(d<dMin){dMin=d;closest=&s->pts[i];}
    }
    return closest;   /* [U] if nb==0, closest is garbage */
}

/* [M] Memory leak: buf is never freed */
/* [N] "tmp_str" then "buffer" then "buf" — three names for the same concept */
char *BuildLabel(Point *p, int id)   /* [N] camelCase again */
{
    char tmp_str[64];
    sprintf(tmp_str,"%s#%d@(%d,%d)",p->nom,id,p->x,p->y);  /* [M] no bounds check */
    char *buf=malloc(strlen(tmp_str)+1);
    if(buf==NULL)return NULL;
    strcpy(buf,tmp_str);
    return buf;
    /* [M] caller must free buf — but there's no documentation of this */
}

int main()   /* [N] should be main(void) */
{
    PointSet S;      /* [N] single uppercase letter — means nothing */
    S.nb=0;

    do_truc(&S,"Alice",10);
    do_truc(&S,"Bob",20);
    do_truc(&S,"Charlie",10);
    do_truc(&S,"Dave",30);

    printf("--- AfficherPoints ---\n");
    AfficherPoints(&S);
    printf("--- print_set ---\n");
    print_set(&S);

    printf("mode 0 (sum): %d\n",process(&S,0));
    printf("mode 1 (max): %d\n",process(&S,1));
    printf("mode 2 (???): %d\n",process(&S,2));  /* [U] garbage returned */

    printf("count valeur==10: %d\n",f(&S,10));

    Point ref; ref.x=50; ref.y=50; ref.valeur=0; strcpy(ref.nom,"ref"); /* [L] cramped */
    Point *cl=find_closest(&S,ref);
    if(cl!=NULL)
        printf("closest to (50,50): %s\n",cl->nom);

    char *lbl=BuildLabel(&S.pts[0],1);
    if(lbl){
        printf("label: %s\n",lbl);
        /* [M] forgot to free(lbl) — memory leak */
    }

    return 0;
}

This is the “full student project” — every smell category appears simultaneously. Use it as a spot-the-bug exercise.

# Run the full toolchain and count warnings
echo "=== gcc ===" && gcc smell_combined.c 2>&1 | wc -l
echo "=== gcc -Wall ===" && gcc -W -Wall -Wextra smell_combined.c 2>&1 | wc -l
echo "=== clang-tidy ===" && clang-tidy --quiet -checks='*' smell_combined.c -- 2>&1 | wc -l
Warning

▶ Wooclap Q12

Show the file annotation legend ([U], [L], [N], [I], [D], [M]) and the file side-by-side.

Ask: “Find 3 [M] (memory) smells in 2 minutes.” Then reveal.

# Fix the layout first (automation helps)
clang-format -i smell_combined.c

# Then run clang-tidy with fixes
clang-tidy --quiet -checks='*' --fix smell_combined.c --

# Memory issues need manual fixes: free(lbl), NULL-guard on find_closest, etc.

4.10 — Incremental GitLab CI Pipeline (50 min)

What Is an “Incremental” Pipeline? (5 min)

A linear pipeline runs stages one by one — if stage 2 fails, stage 3 never starts.

A DAG pipeline (needs:) lets jobs run as soon as their specific dependencies are done, not as soon as the entire previous stage finishes. This is faster for large projects.

Linear:   format → lint → build → test        (each stage waits for ALL previous)
DAG:      format ──┬── lint:tidy ──┐
                   └── lint:check ─┴── build:compile ── build:link ── test:unit
                                                                    └── test:asan

The Pipeline File

.gitlab-ci.yml
# .gitlab-ci.yml — Incremental C project pipeline
#
# Pipeline topology (DAG — not linear stages):
#
#   format ──┐
#             ├── lint:tidy ──┐
#             └── lint:check ─┤
#                              └── build:compile ──┐
#                                                   └── build:link ──┐
#                                                                     ├── test:unit
#                                                                     └── test:asan
#                                                                           └── report:coverage
#
# Key features demonstrated:
#  - Explicit Docker image per job (reproducible environment)
#  - `needs:` for DAG-style execution (jobs run as soon as deps are met)
#  - `rules:` for conditional execution (skip format check on main branch)
#  - `cache:` with a lockfile key (avoid re-installing deps)
#  - `artifacts:` passed between jobs (object files from compile → link)
#  - `allow_failure: true` on lint jobs (warnings don't block the build)
#  - `coverage:` regex to parse lcov output for GitLab's coverage badge

# ── Global defaults ──────────────────────────────────────────────────────────
default:
    image: gcc:13   # default image; individual jobs can override
    before_script:
        - apt-get update -qq
        - apt-get install -y -qq make clang-format clang-tidy cppcheck
              lcov gcovr libcheck-dev 2>/dev/null

# ── Stage declaration (order matters for the pipeline graph) ─────────────────
stages:
    - format
    - lint
    - build
    - test
    - report

# ── Variables ─────────────────────────────────────────────────────────────────
variables:
    CC:      gcc
    CFLAGS:  "-W -Wall -Wextra -Iinclude -MMD"
    LDFLAGS: "-lm"
    BUILD:   build
    SRC_DIR: src
    TEST_DIR: tests

# ── Stage: format ─────────────────────────────────────────────────────────────
# Check that all C sources are correctly formatted with clang-format.
# This is the first gate: if formatting is wrong the rest of the pipeline
# is still triggered (allow_failure: true) but the job is red.
format:check:
    stage: format
    image: silkeh/clang:17   # use a dedicated clang image for format tools
    before_script: []        # skip the global before_script (nothing to install)
    script:
        - echo "=== Checking code formatting ==="
        - >
            clang-format --dry-run --Werror
            $(find $SRC_DIR $TEST_DIR include -name '*.c' -o -name '*.h' 2>/dev/null)
    allow_failure: true      # format errors are warnings, not blockers
    rules:
        # Run on every push except the main branch (main is assumed to be clean)
        - if: $CI_COMMIT_BRANCH != "main"
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# ── Stage: lint ───────────────────────────────────────────────────────────────
# Two lint jobs run in PARALLEL (both depend only on format:check via needs).
# They always run even if format:check failed (needs with optional: true).

lint:tidy:
    stage: lint
    image: silkeh/clang:17
    before_script: []
    needs:
        - job: format:check
          optional: true     # run even if format:check did not run
    script:
        - echo "=== Running clang-tidy ==="
        - >
            clang-tidy --quiet
            $(find $SRC_DIR -name '*.c')
            -- $CFLAGS
    allow_failure: true      # tidy warnings don't block the build

lint:cppcheck:
    stage: lint
    image: neszt/cppcheck-docker   # dedicated cppcheck image
    before_script: []
    needs:
        - job: format:check
          optional: true
    script:
        - echo "=== Running cppcheck ==="
        - >
            cppcheck --enable=warning,style,portability
            --error-exitcode=1
            --suppress=missingIncludeSystem
            -I include
            $SRC_DIR/*.c
    allow_failure: true

# ── Stage: build ──────────────────────────────────────────────────────────────
# Compilation is split into two jobs:
#   1. build:compile  — compiles every .c to .o (parallel if extended)
#   2. build:link     — links all .o files into the final executable
#
# The object files are passed from compile → link via artifacts.

build:compile:
    stage: build
    needs:
        - job: lint:tidy
          optional: true
        - job: lint:cppcheck
          optional: true
    cache:
        key:
            files:
                - $SRC_DIR/*.c
                - include/*.h
        paths:
            - $BUILD/
    script:
        - echo "=== Compiling source files ==="
        - mkdir -p $BUILD
        - >
            for src in $SRC_DIR/*.c; do
                obj="$BUILD/$(basename ${src%.c}.o)";
                echo "  CC $src → $obj";
                $CC $CFLAGS -c "$src" -o "$obj";
            done
    artifacts:
        paths:
            - $BUILD/*.o
            - $BUILD/*.d    # dependency files for incremental rebuilds
        expire_in: 1 hour

build:link:
    stage: build
    needs:
        - build:compile    # wait for .o files
    script:
        - echo "=== Linking ==="
        - mkdir -p $BUILD
        - $CC $BUILD/*.o $LDFLAGS -o $BUILD/project
        - echo "=== Binary size ==="
        - size $BUILD/project
    artifacts:
        paths:
            - $BUILD/project
        expire_in: 1 day

# ── Stage: test ───────────────────────────────────────────────────────────────
# Two test jobs run in PARALLEL once the binary is ready:
#   1. test:unit   — runs the libcheck test suite
#   2. test:asan   — recompiles with AddressSanitizer and runs tests

test:unit:
    stage: test
    needs:
        - build:link
    script:
        - echo "=== Compiling unit tests ==="
        - >
            $CC $CFLAGS --coverage $SRC_DIR/*.c $TEST_DIR/*.c
            -o $BUILD/project_tests -lcheck $LDFLAGS
        - echo "=== Running unit tests ==="
        - ./$BUILD/project_tests
    artifacts:
        paths:
            - $BUILD/*.gcno
            - $BUILD/*.gcda
        expire_in: 1 hour
    coverage: '/^lines:\s+(\d+\.\d+)%/'

test:asan:
    stage: test
    image: silkeh/clang:17
    before_script: []
    needs:
        - build:link        # only needs the link stage to know the build succeeded
    script:
        - echo "=== Recompiling with AddressSanitizer ==="
        - >
            clang -W -Wall -Wextra -g -fsanitize=address,undefined
            -Iinclude
            $SRC_DIR/*.c $TEST_DIR/*.c
            -o $BUILD/project_asan -lcheck $LDFLAGS
        - echo "=== Running ASan ==="
        - ASAN_OPTIONS=halt_on_error=1 ./$BUILD/project_asan
    allow_failure: false    # ASan errors ARE blockers

# ── Stage: report ─────────────────────────────────────────────────────────────
# Generate an HTML coverage report from the gcov data produced by test:unit.
# This job only runs on the main branch or on MRs.

report:coverage:
    stage: report
    needs:
        - test:unit        # need the .gcda files
    rules:
        - if: $CI_COMMIT_BRANCH == "main"
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    script:
        - echo "=== Generating coverage report ==="
        - mkdir -p report
        - gcov -f -b $BUILD/*.gcda
        - lcov --directory $BUILD --base-directory . -c -o report/cov.info
        - lcov --remove report/cov.info '/usr/*' '*/tests/*' -o report/cov_filtered.info
        - genhtml report/cov_filtered.info -o report/html
        - echo "=== Coverage summary ==="
        - lcov --summary report/cov_filtered.info
    artifacts:
        paths:
            - report/html/
        expire_in: 1 week
        reports:
            coverage_report:
                coverage_format: cobertura
                path: report/cobertura.xml
    coverage: '/^\s*lines\.*:\s*(\d+\.\d+)%/'

Walking Through the Pipeline (30 min)

Stage: format (5 min)

format:check:
    image: silkeh/clang:17
    before_script: []        # override global before_script
    script:
        - clang-format --dry-run --Werror $(find src tests include -name '*.c' -o -name '*.h')
    allow_failure: true      # warning, not blocker
    rules:
        - if: $CI_COMMIT_BRANCH != "main"

Key points:

  • before_script: [] — overrides the global apt-get install (clang image already has the tool)
  • --dry-run --Werror — exits 1 if any file would be changed; no file is actually modified
  • allow_failure: true — pipeline stays green, job turns orange (warning)
  • rules: — skip on main (CI assumes main is always clean after MR merge)
Warning

▶ Wooclap Q13

Why use allow_failure: true on the format job but NOT on the build job?


Stage: lint — parallel jobs with needs: (8 min)

lint:tidy:
    needs:
        - job: format:check
          optional: true    # run even if format:check did not produce an artifact

needs: creates a direct dependency between jobs, bypassing stage ordering. optional: true means: “run even if format:check was skipped by its rules:”.

Both lint:tidy and lint:cppcheck start simultaneously once format:check finishes (or is skipped). If the project has many linters, all run in parallel here.

# Local equivalent
clang-tidy --quiet -checks='*' src/*.c -- -W -Wall -Wextra -Iinclude
cppcheck --enable=warning,style,portability --error-exitcode=1 -I include src/*.c

build:compile:
    cache:
        key:
            files:
                - src/*.c
                - include/*.h
        paths:
            - build/

cache: — GitLab caches the build/ directory between pipeline runs. The cache key is derived from the content hash of source files. If no source file changed, the runner reuses the cached .o files = fast incremental build.

    artifacts:
        paths:
            - build/*.o
            - build/*.d
        expire_in: 1 hour

artifacts: — passes files between jobs within the same pipeline. Unlike cache:, artifacts are always fresh (not reused from previous pipelines).

cache:artifacts:
Scopecross-pipelinewithin one pipeline
Purposespeed up installs / buildspass files between jobs
Freshnessmay be stalealways current run
Storagerunner-localGitLab server
Warning

▶ Wooclap Q14

When would you use cache: for object files AND artifacts: at the same time?

Answer: cache: reuses .o from the previous pipeline (skip recompile of unchanged files), artifacts: passes the fresh .o to the next job in the same pipeline.

build:link:
    needs:
        - build:compile    # explicit: wait for .o files to be artifacts
    artifacts:
        paths:
            - build/project
        expire_in: 1 day   # keep the binary for 1 day (download it from GitLab UI)

Stage: test — two parallel test strategies (10 min)

test:unit:
    script:
        - $CC $CFLAGS --coverage src/*.c tests/*.c -o build/project_tests -lcheck $LDFLAGS
        - ./build/project_tests
    coverage: '/^lines:\s+(\d+\.\d+)%/'

--coverage instructs gcc to instrument the binary for gcov. The coverage: regex extracts the percentage from lcov output so GitLab can display it as a badge and track it over time.

test:asan:
    image: silkeh/clang:17
    script:
        - clang -W -Wall -g -fsanitize=address,undefined ...
        - ASAN_OPTIONS=halt_on_error=1 ./build/project_asan
    allow_failure: false   # ASan errors ARE blockers

ASAN_OPTIONS=halt_on_error=1 — stop immediately on the first error (otherwise ASan sometimes continues and produces confusing output).


Stage: report — conditional coverage (5 min)

report:coverage:
    rules:
        - if: $CI_COMMIT_BRANCH == "main"
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Coverage reports are expensive to generate. Run them only on main or on MRs (not on every feature branch push).

    artifacts:
        reports:
            coverage_report:
                coverage_format: cobertura
                path: report/cobertura.xml

The reports: key is special — GitLab parses the file and displays a coverage diff directly in the MR diff view.


Common CI Pitfalls (5 min)

PitfallSymptomFix
Missing needs: on a jobJob waits for the whole previous stage even though it only needs one artifactAdd needs: [job_name]
cache: but no artifacts:Next job can’t find the .o filesartifacts: for within-pipeline, cache: for cross-pipeline
allow_failure: true on buildA broken build is hidden; test runs on nothingOnly use allow_failure on lint/format jobs
No expire_in: on artifactsGitLab fills up with hundreds of binary artifactsAlways set expire_in:
Global before_script: on all jobsLinter image reinstalls gcc unnecessarilyOverride with before_script: [] on jobs that don’t need it

Adding the Pipeline to the Makefile (2 min)

A useful make ci target that mirrors what GitLab does locally:

# Mirrors the GitLab CI pipeline for local pre-push checking
ci: format lint build test

format:
	clang-format --dry-run --Werror $(shell find src include -name '*.c' -o -name '*.h')

lint:
	clang-tidy --quiet -checks='*' src/*.c -- $(CFLAGS)
	cppcheck --enable=warning,style,portability -I include src/*.c

build: $(TARGET)

test: $(EXEC_TESTS)
	./$(EXEC_TESTS)

“Run make ci before every git push — catch problems before GitLab does.”


Recap: The Full Toolchain (3 min)

Source code

   ├─ clang-format   → formatting (layout, spacing)
   ├─ clang-tidy     → static analysis (naming, bugs, CERT)
   ├─ cppcheck       → portability, style

   ├─ gcc -W -Wall   → compiler warnings (uninit, unused, types)
   ├─ gcc --coverage → instrument for coverage

   ├─ ./tests        → unit tests (check.h)
   ├─ ASan/UBSan     → runtime memory / UB detection
   ├─ Valgrind       → memory (when no source / cross-platform)

   └─ lcov/genhtml   → coverage report

All of the above can run in CI automatically on every push — you never have to remember to run them manually.

Info

Take-away: The pipeline is not a quality gate you fight against — it is a tool that tells you exactly where your code hurts, every time you push.