Python Abstract Base Classes in Action

Photo by Markus Spiske on Pexels.com

Abstract base classes

Python has this idea of abstract base classes (ABCs), which define a set of methods and properties that a class must implement in order to be considered a duck-type instance of that class. The class can extend the abstract base class itself in order to be used as an instance of that class, but it must supply all the appropriate methods.

Case study

Recently, I worked on an internal tool to parse source files containing test cases written in three different kinds of testing frameworks i.e. pytest, Robot Framework, and Cucumber. Since the tool needs to parse source files with three different parsers with a same set of APIs, it is advisable to create an abstract base class in this case to document what APIs the parsers should provide (documentation is one of the stronger use cases for ABCs). The abc module provides the tools I need to do this, as demonstrated in the following block of code:

from abc import ABC, abstractmethod
from pathlib import Path


class Parser(ABC):
    @property
    def name(self) -> str:
        return self._name

    @property
    def test_case_indicator(self) -> str:
        return self._test_case_indicator

    @abstractmethod
    def filter_test_case_from_source_file(self, path: Path) -> str:
        pass


class PytestParser(Parser):
    def __init__(self, source_file: Path) -> None:
        self._name = 'pytest'
        self._file_ext = 'py'
        self._test_case_indicator = 'def '

    def filter_test_case_from_source_file(self, path: Path) -> str:
        pass


class RobotParser(Parser):
    def __init__(self, source_file: Path) -> None:
        self._name = 'robot'
        self._file_ext = 'robot'
        self._test_case_indicator = '*** Test Cases ***'

    def filter_test_case_from_source_file(self, path: Path) -> str:
        pass


class GherkinParser(Parser):
    def __init__(self, source_file: Path) -> None:
        self._name = 'gherkin'
        self._file_ext = 'feature'
        self._test_case_indicator = 'Scenario: '

    def filter_test_case_from_source_file(self, path: Path) -> str:
        pass

Though I omitted the implementation of the filter_test_case_from_source_file() method for brevity, the idea is that any subclass of the Parser class must implement this method, as it is decorated with @abstractmethod.

More common object-oriented languages (like Java, Kotlin) have a clear separation between the interface and the implementation of a class. For example, some languages provide an explicit interface keyword that allows us to define the methods that a class must have without any implementation. In such an environment, an abstract class is one that provides both an interface and a concrete implementation of some, but not all, methods. Any class can explicitly state that it implements a given interface.
Python’s ABCs help to supply the functionality of interfaces without compromising on the benefits of duck typing.

References

PEP-3119

Executing System Command in Cypress Tests

Photo by Darren Halos on Pexels.com

Recently, I wrote a Cypress test where I had to copy a configuration file from one container to another container. As you may know, the easiest way to do that is to use the “docker cp” command. This post is a step-by-step how-to I used to achieve this.

Installing Docker

The “tester” container is based on the official cypress/included:6.3.0 docker image, which in turn is based on the official node:12.18.3-buster docker image. So as the first step, I had to figure out how to install Docker in Debian 10 in order to be able to run “docker cp” from within the container:

FROM cypress/included:6.3.0

RUN apt-get update && apt-get install -y \
  apt-transport-https \
  gnupg2 \
  software-properties-common

RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -

RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable"

RUN apt-get update && apt-get install -y \
  docker-ce

Creating a Volume for the /var/run/docker.sock

In order to talk to the Docker daemon running outside of the “tester” container, I had to add a volume to mount the famous /var/run/docker.sock in the docker compose file:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Executing “docker cp” in Cypress Test

Finally, I was able to execute “docker cp” to copy the configuration file from the “tester” container to the “web_app” container using the Cypress exec command:

const configYamlPath = 'cypress/fixtures/config.yaml';

cy.exec(`docker cp ${configYamlPath} web_app:/opt/web_app`)
  .then(() => cy.reload());

Want to buy me a coffee? Do it here: https://www.buymeacoffee.com/j3rrywan9

JavaScript Object Literal Shorthand with ECMAScript 2015

Photo by Markus Spiske on Pexels.com

Last Friday, I hit an ESLint error as below:

335:13  error  Expected property shorthand        object-shorthand

Per the “object-shorthand” rule’s documentation, there is a syntactic sugar for defining object literal methods and properties in the ECMAScript 2015 (ES6).

Quite often, when declaring an object literal, property values are stored in variables whose names are equal to the property names. For example:

const timeout = 30000;

const options = { timeout: timeout };

There is a shorthand for this situation:

const timeout = 30000;

const options = { timeout };

References

Installing Ruby from Source on Debian 8 Using SaltStack

Photo by Castorly Stock on Pexels.com

The default Ruby shipped with Debian 8 is of version 2.1.5, which is very old. You can use the following SaltStack states to install Ruby 2.5.1 from source:

bison:
  pkg.installed

libgdbm-dev:
  pkg.installed

libreadline-dev:
  pkg.installed

libssl-dev:
  pkg.installed

openssl:
  pkg.installed

zlib1g-dev:
  pkg.installed

download_ruby_2.5.1_source:
  cmd.run:
    - name: curl -s -S --retry 5 https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.1.tar.gz | tar xz
    - runas: jenkins
    - cwd: /var/lib/jenkins
    - unless: command -v ruby && test '2.5.1p57' = $(ruby -v|awk '{print $2}')

install_ruby_2.5.1_from_source:
  cmd.run:
    - name: cd /var/lib/jenkins/ruby-2.5.1 && ./configure && make && make install
    - onchanges:
      - download_ruby_2.5.1_source

remove_ruby_2.5.1_source:
  file.absent:
    - name: /var/lib/jenkins/ruby-2.5.1
    - onchanges:
      - download_ruby_2.5.1_source

References

Adding Multiple Lines to a File using Ansible

Photo by Pixabay on Pexels.com

The Ansible module lineinfile will search a file for a line and ensure that it is present or absent. It is useful when you want to change a single line in a file only. But how to add multiple lines to a file? You can use a loop to do this together with lineinfile like the following:

- name: ASE Deps | Configure sudoers
  lineinfile:
    dest: /etc/sudoers
    line: "{{ item }}"
  with_items:
    - "Defaults:sybase !requiretty"
    - "sybase ALL=(ALL) NOPASSWD: /bin/mount, /bin/umount, /bin/mkdir, /bin/rmdir, /bin/ps"

Want to buy me a coffee? Do it here: https://www.buymeacoffee.com/j3rrywan9

Configuring DNS when DHCP is Used on Ubuntu

Photo by panumas nikhomkhai on Pexels.com

When eth0 is configured to use DHCP on Ubuntu (14.04 LTS), the contents of /etc/resolv.conf are overwritten by resolvconf (man 8 resolvconf), which in turn is called by dhclient. So you can neither set “dns-nameservers” and “dns-search” in /etc/resolv.conf nor /etc/network/interfaces.d/eth0.cfg.

The solution is to supersede the “domain-name-servers” and “domain-search” values in /etc/dhcp/dhclient.conf (man 5 dhclient.conf):

supersede domain-name-servers 172.16.101.11;
supersede domain-search "example.com";

And you may need to renew DHCP lease to make above change effective:

sudo dhclient -r eth0
sudo dhclient eth0

Passwordless SSH to Git Server

Photo by Pixabay on Pexels.com

Generate RSA key for the user “git” on your Git server.

Add following lines to your ssh_config:

Host git git.example.com
  User git
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  IdentitiesOnly yes
  IdentityFile /etc/ssh/id_rsa_insecure

Where file id_rsa_insecure contains the private key of the user “git” on your Git server.

Changing Database ID of an Oracle Database

Photo by Manuel Geissinger on Pexels.com

DBNEWID is a database utility that can change the internal database identifier (DBID) and the database name (DBNAME) for an Oracle database.

ora11202@bbdhcp:/home/ora11202-> sqlplus / as sysdba

SQL*Plus: Release 11.2.0.2.0 Production on Tue Jul 14 20:44:21 2015

Copyright (c) 1982, 2010, Oracle. All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> shutdown immediate;
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup mount
ORACLE instance started.

Total System Global Area 634679296 bytes
Fixed Size 2229160 bytes
Variable Size 385879128 bytes
Database Buffers 243269632 bytes
Redo Buffers 3301376 bytes
Database mounted.
SQL> exit
Disconnected from Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
ora11202@bbdhcp:/home/ora11202-> nid target=/

DBNEWID: Release 11.2.0.2.0 - Production on Tue Jul 14 20:45:30 2015

Copyright (c) 1982, 2009, Oracle and/or its affiliates. All rights reserved.

Connected to database DBDHCP3 (DBID=528973516)

Connected to server version 11.2.0

Control Files in database:
 /datafile/dbdhcp3/oradata/dbdhcp3/control01.ctl
 /datafile/dbdhcp3/oradata/dbdhcp3/control02.ctl

Change database ID of database DBDHCP3? (Y/[N]) => Y

Proceeding with operation
Changing database ID from 528973516 to 632686762
 Control File /datafile/dbdhcp3/oradata/dbdhcp3/control01.ctl - modified
 Control File /datafile/dbdhcp3/oradata/dbdhcp3/control02.ctl - modified
 Datafile /datafile/dbdhcp3/oradata/dbdhcp3/system01.db - dbid changed
 Datafile /datafile/dbdhcp3/oradata/dbdhcp3/sysaux01.db - dbid changed
 Datafile /datafile/dbdhcp3/oradata/dbdhcp3/undotbs01.db - dbid changed
 Datafile /datafile/dbdhcp3/oradata/dbdhcp3/users01.db - dbid changed
 Datafile /u03/app/ora11202/product/11.2.0/dbhome_1/dbs/dbv_R2V4.db - dbid changed
 Datafile /datafile/dbdhcp3/oradata/dbdhcp3/temp01.db - dbid changed
 Control File /datafile/dbdhcp3/oradata/dbdhcp3/control01.ctl - dbid changed
 Control File /datafile/dbdhcp3/oradata/dbdhcp3/control02.ctl - dbid changed
 Instance shut down

Database ID for database DBDHCP3 changed to 632686762.
All previous backups and archived redo logs for this database are unusable.
Database is not aware of previous backups and archived logs in Recovery Area.
Database has been shutdown, open database with RESETLOGS option.
Succesfully changed database ID.
DBNEWID - Completed succesfully.

Time Conversion in Go

Photo by David Bartus on Pexels.com

Convert seconds (integer) to hour:minute:second formatted string:

package main

import (
    "fmt"
    "strconv"
    "time"
)

func convertElapsedSecondsToString(seconds int64) string {
    timeElapsedString := strconv.FormatInt(seconds, 10) + "s"
    duration, err := time.ParseDuration(timeElapsedString)
    if err != nil {
        panic(err)
    }
    hour := int64(duration/time.Hour)
    min := int64(duration/time.Minute) - hour * 60
    sec := seconds - hour * 60 * 60 - min * 60
    h := strconv.FormatInt(hour, 10)
    m := strconv.FormatInt(min, 10)
    s := strconv.FormatInt(sec, 10)
    return fmt.Sprintf("%s:%s:%s", h, m, s)
}

func main() {
    myTestRunDuration := convertElapsedSecondsToString(20555)
    fmt.Println(myTestRunDuration)
}