ExUnit capture log và Erlang IO system
elixir
37
Erlang
15
White

Cẩm Huỳnh viết ngày 26/05/2019

Như thường lệ, bài viết được đăng lại từ blog Quần Cam.


alt text

Budapest Chain Bridge by Bergadder on Pixabay

ExUnit

ExUnit là một unit testing framework được ship chung với ngôn ngữ Elixir. Nó hỗ trợ khá nhiều tính năng cần thiết trong kiểm thử như: assertions, doc tests, formatters, filters, IO capturing, test case templating ... Trên tất cả, tính năng nổi bật nhất của framework này là khả năng chạy đồng thời nhiều test case. Test case trong một module được khai báo async: true sẽ được chạy cùng lúc với nhau khi máy tính của bạn có nhiều nhân CPU. Nhờ đó, test suite chạy nhanh hơn, nâng cao hiệu suất làm việc của lập trình viên.

defmodule MyTestCase do
  use ExUnit.Case, async: true

  test "1", do: assert(1 == 1)

  test "2", do: refute(2 == 1)
end

Capture Log

Đôi lúc khi lập trình, bạn có nhu cầu muốn bắt lại log để phục vụ cho mục đích kiểm thử. Để làm điều này, ExUnit hỗ trợ hàm tiện ích ExUnit.CaptureLog.capture_log/1, với đầu vào là một anonymous function. Hàm này sử dụng CaptureIO ở bên dưới, bắt hết tất cả những gì xuất ra STDOUT và trả chúng về cho bạn dưới dạng string.

test "logs when some thing happens" do
  log =
    ExUnit.CaptureLog.capture_log(fn ->
      MyModule.my_function()
    end)

  assert log =~ "Logs from MyModule.my_function()"
end

Tính năng này không có gì mới. Hầu hết các ngôn ngữ hiện hành đều hỗ trợ nó dưới dạng này hay dạng khác. Ví dụ như thư viện kiểm thử nổi tiếng RSpec của Ruby với matcher output. Matcher này hoạt động bằng cách tạm thời ghi đè vào biến $stdout của máy ảo Ruby. Tuy nhiên, cách hiện thực này có hai vấn đề lớn:

  1. Hệ thống của chính bạn có thể bị banh nếu bạn không biết rõ mình đang làm gì: $stdout là một biến toàn cục được sử dụng bởi nhiều thành phần trong hệ thống và bạn thì đang ... ghi đè nó.
  2. Khi chạy đồng thời nhiều test case, bạn có thể lỡ tay gom nhầm output của một test case khác. Qua đó giảm tính đúng đắn của cả hai bên.

Là một framework đặt nặng khả năng chạy song song, ExUnit không ghi đè một biến toàn cục nào cả mà hiện thực tính năng này theo một hướng khác dựa trên Erlang IO system.

Erlang IO system

Khi một process cần nhập xuất, nó không tương tác trực tiếp với device (STDOUT, STDERR, socket, ...). Thay vào đó nó sẽ gửi request tới IO server. Các IO server này là trung tâm của hệ thống, đảm nhận tương tác với device tương ứng. Mọi thao tác liên quan tới IO server đều được thực hiện thông qua interface :io trong Erlang hoặc IO trong Elixir.

Chi tiết về giao thức IO các bạn có thể xem trên trang chủ của Erlang.

alt text

Sự phân tách này nhằm khái quát hóa thao tác IO trong lập trình Elixir/Erlang. Mọi process thực hiện nhập xuất bằng cách gửi request tới IO server mà không cần biết bên dưới trao đổi với device thế nào. Khi nhận được request, IO server sẽ đảm nhận tương tác với device tương ứng. Nó có thể xử lý, định dạng sao đó hợp lý, và trả lời lại cho process gửi nếu cần. Mọi thao tác đều xảy ra bất đồng bộ như bản chất của Erlang.

Hơn nữa, sự khái quát hóa này còn giúp dễ dàng tái sử dụng các IO server. Đơn cử như logger và TCP sockets có thể sử dụng chung IO server nếu chúng nhập xuất tới cùng một device.

Bên cạnh đó, việc redirect IO trở nên đơn giản hơn rất nhiều. Ứng dụng điển hình nhất chính là remote console, một tiện ích giúp bạn attach interactive shell vào một Erlang node đang chạy. Một node có thể đang route IO về X nhưng khi bạn thực thi một command trong remote console, IO (prompt/output/log) sẽ được redirect tới chính console đó.

Remote console của Erlang ngoài việc dùng để test code còn làm được một thứ rất "cool" là runtime tracing. Bạn có thể attach một shell vào Erlang node đang chạy và bind tracer vào một process bất kì trong node. Các thông tin tracing sẽ được gửi đến bằng messages. Cơ mà ... lan man rồi, tui xin hẹn viết trong một bài khác.

Group leader

Trong Erlang có một khái niệm là group leader. Mặc định mỗi Erlang application khi được start đều thuộc về một group và group có leader. Một group leader có thể thuộc về một group leader khác. Truy cùng vét tận, ta tới được "master process" là group leader tối cao, được bật cùng với Erlang node.

Một Erlang process không nằm chỏng chơ ở một hốc bò tó nào đó trong VM mà luôn thuộc về một group leader. Khi một process con được spawn, nó thừa hưởng group leader từ process cha.

Nhờ group leader, khi tắt một application, máy ảo Erlang biết cách thu hồi bộ nhớ sạch sẽ và tránh được memory leak. Hẳn đọc tới đây có bạn sẽ đặt câu hỏi: "Chẳng phải ông hay rêu rao process trong Erlang được supervise sao?". Èo, start process bằng spawn/1 thì bạn chỉ có thể supervise nó bằng răng thôi.

Ngoài mục đích thu hồi bộ nhớ, group leader còn là IO server cho STDIO device. Khi môt process gọi IO.puts(:stdio, "message"), một IO request put_chars sẽ được gửi đến group leader của nó. Nếu group leader này thuộc về một group leader khác, nó chỉ đơn giản là forward message lên trên. Và cuối cùng khi đến "master", message sẽ được gửi đến STDIO device.

CaptureIO

Đến đây chắc bạn cũng có thể tự giải thích cho câu hỏi đầu bài.

Khi test case chạy và yêu cầu capture IO, ExUnit sẽ thay đổi group leader của nó bằng một StringIO server. Mọi IO requests diễn ra sau đó sẽ được redirect về server này. Chúng được gom lại và trả về dưới dạng string khi kết thúc hàm.

defp do_capture_io(:standard_io, options, fun) do
  # lược bỏ vì tui không thích...

  original_gl = Process.group_leader()
  {:ok, capture_gl} = StringIO.open(input, capture_prompt: prompt_config)

  try do
    # đổi group leader.
    Process.group_leader(self(), capture_gl)

    # bắt đầu chạy.
    do_capture_io(capture_gl, fun)
  after
    # cài lại group leader cũ.
    Process.group_leader(self(), original_gl)
  end
end

Thật là tuyệt vời đúng không nào?

Bài viết này sẽ giúp bạn tăng lương thế nào?

Bài viết này như thường lệ không giúp bạn tăng lương. Cơ mà mong là cùng nhau ta đã biết thêm một chút về vẻ đẹp của Erlang.

Tui sẽ bonus ba hoa thêm một chút về cách Erlang remote shell redirect IO. Khi bạn start một shell, group leader mặc định là chính nó. Như vậy, mọi thao tác với STDIO trong phạm vi shell sở tại đều không ảnh hưởng gì đến hệ thống đang chạy.

Tham khảo

  1. Erlang Rationale của Robert Virding.
  2. The Erlang I/O Protocol.
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Cẩm Huỳnh

47 bài viết.
454 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
66 5
image cover]imgcover] “Make it work, make it right, make it fast.” Bạn vừa viết xong một ứng dụng web :tada:. Mọi thứ chạy ổn. Code cũng đã được...
Cẩm Huỳnh viết 10 tháng trước
66 5
White
47 26
Vừa rồi mình vừa tiết kiệm được $5 mỗi tháng sau khi migrate cái (Link) từ Digital Ocean sang Heroku Free Dyno. (Ảnh) Kết quả thật mĩ mãn vì hầu ...
Cẩm Huỳnh viết 2 năm trước
47 26
White
44 9
(Ảnh) Vì sao lại là Bật Đèn? Ai từng đọc qua Tắt Đèn hẳn đã biết tác phẩm được kết thúc bằng tình huống: Buông tay, chị vội choàng dậy, mở cửa...
Cẩm Huỳnh viết 2 năm trước
44 9
Bài viết liên quan
White
9 6
Chưa xem phần 2? Xem (Link) Trong bài viết này tôi giới thiệu cho các bạn về khái niệm function arity, một cách gọi mĩ miều của số lượng argument ...
Lơi Rệ viết gần 4 năm trước
9 6
White
5 0
Chú thích: Bài này đăng lần đầu năm 2009 để chia sẻ kinh nghiệm, sau khi tác giả viết xong thư viện closed source để bán. Hiện tại năm 2016 đã có t...
Ngoc Dao viết hơn 3 năm trước
5 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
47 bài viết.
454 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!